#!/usr/bin/env bash
#========================================================
# 脚本名     :   DMShellInstall
# 创建时间   :   2022-11-03 10:02:09
# 更新时间   :   2025-07-23 00:30:00
# 描述      :   达梦数据库一键安装脚本（单机/数据守护[raft主备]/DSC）
# 路径      :   /soft/DMShellInstall
# 版本      :   1.0.0
# 作者      :   Lucifer(pc1107750981@163.com) yuanzijian(yzj@dameng.com) xiefenfen(xff@dameng.com)
# Copyright (C) 2021-2023 Pengcheng Liu       Zijian Yuan                Fenfen Xie
#========================================================
#========================================================
# 导出 PS4 变量，以便 set -x 调试时输出行号和函数参数
export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
#========================================================
# 全局变量
#========================================================
# 数据库的节点号，默认为 1
node_num=1
# 安装脚本以及软件包存放位置
software_dir=$(dirname "$(readlink -f "$0")")
# 删除脚本生成的LOG日志文件
find "$software_dir" -name "print_dm_install_*.log" -exec rm -rf {} +
# 达梦数据库安装日志
dminstalllog="${software_dir}"/print_dm_install_$(date +"%Y%m%d%H%M%S").log
# 系统当前时间
current=$(date +%Y%m%d%H%M%S)
# 定义服务器标记
os_flag=OS1
# 服务器类型
os_type=rhel
# 操作系统版本
os_version=7
# 服务器物理内存大小
os_memory=$(awk '/MemTotal/{print $2}' /proc/meminfo)
# 主机名
hostname=
# 执行脚本密码
root_passwd=
# 存储生成的随机字符串
random_string=$(tr -dc '[:lower:]' </dev/urandom | head -c 5)
# 主机用户dmdba密码，默认 Dameng@123
dm_passwd=Dameng@123
# 达梦数据库名称，默认 DAMENG
db_name=DAMENG
# 实例名，默认 DMSERVER
instance_name=DMSERVER
# 数据文件使用的簇大小(32)，可选值：16, 32, 64，单位：页
extent_size=32
# 数据页大小(32)，可选值：4, 8, 16, 32，单位：K
page_size=32
# 大小写敏感(Y)，可选值：Y/N，1/0
declare -u case_sensitive
case_sensitive=Y
# 字符集(1)，可选值：0[GB18030]，1[UTF-8]，2[EUC-KR]
charset=1
## VARCHAR类型长度是否以字符为单位(N)，可选值：Y/N，1/0
declare -u length_in_char
length_in_char=
#自定义参数
dminit_para_value=
# 0：不兼容，1：兼容SQL92标准，2：部分兼容ORACLE，3：部分兼容MS SQL SERVER，4：部分兼容MYSQL，5：兼容DM6，6：部分兼容TERADATA，7：部分兼容POSTGRES
compatible_mode=0
# 设置空格填充模式(0)，可选值：0/1
blank_pad_mode=0
# 日志文件大小(1024)，单位为：M，范围为：256M ~ 2G
log_size=1024
# redo日志后缀名
rlog_postfix_name=rlog
# 是否开启归档模式，默认开启
declare -u enable_arch
enable_arch=Y
# 归档空间大小，单位M，默认值 102400M
space_limit=102400
# 监听端口号(5236)
port_num=5236
# 达梦数据库用户SYSDBA临时密码
sysdba_pwd_temp=Dm@SYSDBA123
# 达梦数据库用户SYSDBA密码
sysdba_pwd=
# 达梦数据库用户SYSAUDITOR临时密码
sysauditor_pwd_temp=Dm@SYSAUD123
# 达梦数据库用户SYSAUDITOR密码
sysaud_pwd=
# 达梦数据库用户SYSSSO临时密码
syssso_pwd_temp=Dm@SYSSSO123
# 达梦数据库用户SYSSSO密码
syssso_pwd=
#判断数据库是是安全版,默认否，即数据库默认是企业版
db_flag=false
# 达梦软件安装目录，默认 /home/dmdba/dmdbms
env_dm_home=/home/dmdba/dmdbms
# 数据库存放目录，默认 /dmdata
data_dir=/dmdata
# 数据库备份目录，默认 /dmbak
backup_dir=/dmbak
# 数据库归档目录，默认 /dmarch
arch_dir=/dmarch
# 数据库core文件目录，默认 /dmbak
core_dir=
# 数据库用户脚本目录，默认 /home/dmdba/scripts
scripts_dir=/home/dmdba/scripts
# 数据库备份模式，1是全备，2是增量，默认 2
backup_mode=2
# 时间服务器IP
timeserver_ip=''
# DSC第三方确认IP
link_check_ip=''
# 仅配置操作系统，不安装达梦软件以及数据库，默认为 N
declare -u only_conf_os
only_conf_os=N
# 仅安装达梦数据库软件，不创建数据库，默认为 N
declare -u install_until_db
install_until_db=N
# 仅初始化数据库，默认为 N
declare -u only_init_db
only_init_db=N
# 建好数据库后，优化数据库，默认为 Y
declare -u optimize_db
optimize_db=Y
# 优化数据库时，内存占比百分数，默认为 80
mem_percent=80
# 并发量较高的OLTP类型系统此参数设置为1，并发量不高的一般业务系统和OLAP类的系统此参数设置为0,影响SORT_FLAG和UNDO_RETENTION
oltp_mode=0
# 达梦安装镜像名称，默认为空
dm_iso_name=''
# 达梦密钥路径
key_path=''
# 安装模式
declare -l dm_install_mode
# 添加一个参数校验是否合格的标志，函数中只会修改为0，不会再修改为1；
ARG_CHECK_FLAG=1
# 定义调试模式
declare -u debug_flag
debug_flag=N
# 定义是否开启os认证
declare -u os_auth
os_auth=Y
# 数据库版本
db_version=8
# 默认不配置大页内存，默认值为 N
declare -u huge_flag
huge_flag=N
# 存储服务器配置文件信息
declare -A sysinfo
# JS脚本名称
js_data=dmData.js
# 是否需要ping
declare -u is_nec_ping
is_nec_ping=Y
#========================================================
# 数据守护[raft主备] && DSC 部署全局变量
#========================================================
# 其他节点执行命令
node_cmd=
# 全局业务网络IP数组
declare -a all_pubip_arr
# 全局心跳网络IP数组
declare -a all_malip_arr
# 全局业务网络IP数组
declare -a all_args_pubip_arr
# 全局心跳网络IP数组
declare -a all_args_malip_arr
# 实时/即时/主备备业务IP（DSC业务IP）
db_public_ip=
# 实时/即时主备心跳IP（DSC心跳IP）
db_mal_ip=
# 实时/即时主备数组（DSCIP地址数组）
declare -a db_pubip_arr
declare -a db_malip_arr
# 异步主备业务IP
as_public_ip=
# 异步主备业务IP
as_mal_ip=
# 异步备库数组
declare -a as_pubip_arr
declare -a as_malip_arr
# 同步主备业务IP
sy_public_ip=
# 同步主备业务IP
sy_mal_ip=
# 同步备库数组
declare -a sy_pubip_arr
declare -a sy_malip_arr
# 确认监视器业务IP
dw_monitor_ip=
# 定义标签值，默认为0
os_flag_value=0
# 操作系统 ISO 是否需要挂载，默认为 Y
declare -u iso
iso=Y
# 定义ssh端口
os_ssh_port=22
# 定义sftp服务器IP地址
sftp_server_ip=
# 定义sftp服务器PORT端口
sftp_server_port=22
# 定义sftp用户名
sftp_username=
# 定义sftp本地服务器密码
sftp_passwd=
# 定义sftp本地服务器软件路径
sftp_dir=
# 安装sshpass包
declare -u only_install_sshpass
only_install_sshpass=N
# 是否使用sshpass，默认不使用
declare -u is_sshpass
is_sshpass=N
# 指定已存在的互信
ssh_key_path=
# 生成6位数随机正整数，且首位不是0
oguid=$(shuf -i 100000-999999 -n 1)
#========================================================
# 数据守护[raft主备]全局变量
#========================================================
#  守护进程组名
dw_group_name=GRP1
#  MAL 监听端口，默认为 5336
mal_port_num=5336
# 守护进程端口，默认为 5436
mal_dw_port_num=5436
# 实例监听守护进程端口，默认为 5536
mal_inst_dw_port_num=5536
# 数据守护切换模式，默认为手动
declare -u dw_mode
dw_mode=MANUAL
# 数据守护性能模式
arch_wait_apply=1
# 数据守护归档模式
arch_type=0
declare -u dw_arch_type
dw_arch_type=REALTIME
# 主库向异步备库发送归档时间间隔
repeat_interval=60
# 同步备库异步恢复的时间间隔
arch_recover_time=1
# 仅创建用户
declare -u only_create_user
only_create_user=N
#========================================================
# raft主备全局变量
#========================================================
# raft组内实例通信心跳校验间隔
raft_hb_interval=150
# raft组内实例基础投票间隔
raft_vote_interval=3000
# xmal通信心跳校验间隔
xmal_hb_interval=5
# xmalip地址地址属组
declare -a xmal_ip_arr
# xmalip地址
xmal_ip=
# xmal监听端口
xmal_port=10006
# 归档目标监听端口
arch_dest_port=10006
#========================================================
# DSC 全局常量
#========================================================
# udev规则文件路径
UDEV_RULES_PATH="/etc/udev/rules.d"
# ASM设备组路径
ASM_DEVICE_PATH="/dev/asmdisk"
# udev权限规则文件名
DM_PERMISSIONS_RULES="12-dm-permissions.rules"
# udev ASM设备规则文件名
DM_ASMDEVICES_RULES="99-dm-asmdevices.rules"
# multipath配置文件路径
MULTIPATH_CONF_PATH="/etc/multipath.conf"
#========================================================
# DSC 全局变量
#========================================================
# 定义心跳网和主机别名数组
declare -a malip_host_arr
# 定义DSC集群db_name
declare -u dsc_db_name
dsc_db_name=DSC
# 定义节点实例名前缀，建议和DB_NAME保持一致
declare -l dsc_ep_name
dsc_ep_name=DSC
# 是否格式化共享存储盘
declare -u format_disk
format_disk=Y
# 需要过滤的磁盘
declare -l fil_disk
# 定义dcr磁盘
declare -l dcr_disk
dcr_disk=
# 定义vote磁盘
declare -l vote_disk
vote_disk=
# 定义redo日志磁盘
declare -l log_disk
log_disk=
# 定义归档磁盘
declare -l arch_disk
arch_disk=
# 定义数据磁盘
declare -l data_disk
data_disk=
# 定义磁盘数组
declare -a disk_arr
# 定义asm磁盘数组
declare -a asm_disk_arr
# 定义ASM实例密码
asm_pwd=Dm@ASM12321
# 定义css_dcr_ep_port端口
css_dcr_ep_port=9341
# 定义asm_dcr_ep_port端口
asm_dcr_ep_port=9351
# 定义db_dcr_ep_port端口
db_dcr_ep_port=9361
# 定义asvrmal_mal_port端口
asvrmal_mal_port=9451
# 定义db_mal_port端口
db_mal_port=9461
# 定义多路径参数
declare -u multipath
multipath=N
# 是否配置本地归档
declare -u is_local_arch
is_local_arch=N
# 数据分配单元
au_size=4
# DMDASM实例日志大小
redo_size=128
# redo日志的冗余模式
redo_redun=1
# 归档盘的冗余模式
arch_redun=1
# 数据盘的冗余模式
data_redun=1
# SYSTEM/MAIN/ROLL 表空间数据文件副本数
data_mirror=1
# 联机日志文件副本数
log_mirror=1
# 数据文件条带化粒度
data_striping=32
# 联机日志条带化粒度
log_striping=64
# dm.ctl 和 dm_service.prikey 文件副本数
ctl_mirror=1
# 归档文件配置在 ASM 上镜像
arch_asm_mirror=1
# 归档文件配置在 ASM 上条带化类型
arch_asm_striping=64
# 是否使用别名代替心跳网络
declare -u alias_replaces_ip
alias_replaces_ip=N
# 获取cp命令绝对路径
CP_CMD=$(type -P cp)
#========================================================
# Art Print
#========================================================
function script_print() {
	printf "\n"
	cat <<'EOF'
 ███████   ████     ████  ████████ ██               ██  ██ ██                    ██              ██  ██
░██░░░░██ ░██░██   ██░██ ██░░░░░░ ░██              ░██ ░██░██                   ░██             ░██ ░██
░██    ░██░██░░██ ██ ░██░██       ░██       █████  ░██ ░██░██ ███████   ██████ ██████  ██████   ░██ ░██
░██    ░██░██ ░░███  ░██░█████████░██████  ██░░░██ ░██ ░██░██░░██░░░██ ██░░░░ ░░░██░  ░░░░░░██  ░██ ░██
░██    ░██░██  ░░█   ░██░░░░░░░░██░██░░░██░███████ ░██ ░██░██ ░██  ░██░░█████   ░██    ███████  ░██ ░██
░██    ██ ░██   ░    ░██       ░██░██  ░██░██░░░░  ░██ ░██░██ ░██  ░██ ░░░░░██  ░██   ██░░░░██  ░██ ░██
░███████  ░██        ░██ ████████ ░██  ░██░░██████ ███ ███░██ ███  ░██ ██████   ░░██ ░░████████ ███ ███
░░░░░░░   ░░         ░░ ░░░░░░░░  ░░   ░░  ░░░░░░ ░░░ ░░░ ░░ ░░░   ░░ ░░░░░░     ░░   ░░░░░░░░ ░░░ ░░░
EOF
	printf "\n"
	color_printf red "注意：本脚本仅用于新服务器上实施部署数据使用，严禁在已运行数据库的主机上执行，以免发生数据丢失或者损坏，造成不可挽回的损失！！！"
}
#========================================================
# 工具：run_cmd
# 逻辑：统一封装所有需要 root 权限的命令调用
# 逻辑：根据 debug_flag 开关 bash -x 调试输出
# 逻辑：非 root 用户自动加 sudo，root 用户直接执行
# 逻辑：始终将真实命令的退出码原样返回给调用者
#========================================================
function run_cmd() {
	# 若用户要求调试，则打开 set -x 打印每条命令
	if [[ $debug_flag =~ [Yy] ]]; then
		set -x
	fi
	# 根据当前 UID 决定是否需要 sudo
	if [ "$EUID" -ne 0 ]; then
		sudo "$@"
	else
		"$@"
	fi
	# 捕获真实命令的退出状态
	local exit_status=$?
	# 若之前打开了调试，则关闭 set -x
	if [[ $debug_flag =~ [Yy] ]]; then
		set +x
	fi
	# 把退出状态返回给调用者，保证调用链可感知失败
	return $exit_status
}
#========================================================
# 工具：color_printf
# 逻辑：根据颜色名输出带 ANSI 颜色的格式化文本
# 逻辑：最多支持 3 列左对齐输出，不足列留空
# 逻辑：颜色不存在时默认使用白色
#========================================================
function color_printf() {
	# 定义颜色映射表（键=颜色名，值=ANSI 控制码）
	local -A color_map=(
		["red"]='\033[31m'
		["green"]='\033[1;32m'
		["yellow"]='\033[33m'
		["blue"]='\033[34m'
		["light_blue"]='\033[1;94m'
		["white"]='\033[37m'
		["reset"]='\033[0m'
	)
	# 取颜色，若不存在则回退到白色
	local color=${color_map[$1]:-${color_map["white"]}}
	# 收集最多 3 个文本参数，缺失时留空
	local text1="${2:-}"
	local text2="${3:-}"
	local text3="${4:-}"
	# 按固定宽度左对齐打印，最后重置颜色
	printf "${color}%-15s %-25s %-30s${color_map["reset"]}\n" "$text1" "$text2" "$text3"
}
#========================================================
# 工具：log_print
# 逻辑：生成带边框的醒目日志横幅
# 逻辑：横幅宽度固定 60 字符，绿色高亮
#========================================================
function log_print() {
	local message="$1"
	# 固定颜色与横幅长度
	local color="green"
	local length=60
	# 生成由 '=' 组成的边框字符串
	# shellcheck disable=SC2155
	local string=$(printf "=%.0s" $(seq 1 $length))
	# 打印空行 + 上边框 + 消息 + 下边框 + 空行
	printf "\n"
	color_printf "$color" "#${string}#"
	color_printf "$color" "${message}"
	color_printf "$color" "#${string}#"
	printf "\n"
}
#========================================================
# 工具：die
# 逻辑：检测参数值是否为空或以“-”开头
#========================================================
function die() {
	ARG_CHECK_FLAG=0
	printf "\n"
	color_printf "red" "$*"
}
#========================================================
# 工具：checkpara_null
# 逻辑：检测参数值是否为空或以“-”开头
#========================================================
function checkpara_null() {
	local para="$1"
	local value="$2"
	# 若值为空或以 - 开头，立即报错退出
	if [[ -z ${value} || ${value} == -* ]]; then
		printf "\n"
		color_printf red "请检查参数 [ ${para} ] 的值是否为空！"
		exit 1
	fi
}
#========================================================
# 工具：checkpara_yn
# 逻辑：确保参数值只能是 Y/y 或 N/n
#========================================================
function checkpara_yn() {
	local para="$1"
	local value="$2"
	# 正则校验失败则置位 ARG_CHECK_FLAG 并提示
	if ! [[ ${value} =~ ^[YyNn]$ ]]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "请检查参数 [ ${para} ] 的值 ${value} 是否为 [Y/N]."
	fi
}
#========================================================
# 工具：checkpara_validity
# 逻辑：判断参数值是否在合法列表内
#========================================================
function checkpara_validity() {
	local para="$1"
	local value="$2"
	local valid_values=("${@:3}")
	# 利用字符串包含方式快速校验
	if ! [[ " ${valid_values[*]} " =~ ${value} ]]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "请检查参数 [ ${para} ] 的值 ${value} 是否为 [ ${valid_values[*]} ] 中的一个！"
	fi
}
#========================================================
# 工具：validate_str
# 逻辑：校验字符串是否全小写且长度 ≤10
#========================================================
function validate_str() {
	local para="$1"
	local value="$2"
	# 正则匹配失败则置位 ARG_CHECK_FLAG 并提示
	if [[ "${value}" =~ ^[a-z]{1,10}$ ]]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "请检查参数 [ ${para} ] 的值 ${value} 是否为是小写，且长度小于10！"
	fi
}
#========================================================
# 工具：validate_number
# 逻辑：统一的数值验证函数，支持多种验证规则
# 参数：$1-参数名 $2-参数值 $3-验证类型 $4-最小值 $5-最大值
#========================================================
function validate_number() {
	local para="$1"
	local value="$2"
	local validate_type="$3"
	local min="${4:-0}"
	local max="$5"
	local is_valid=1
	local error_msg=""
	case "$validate_type" in
	"INT_0")
		# 验证正整数且大于min
		if ! [[ $value =~ ^[1-9][0-9]*$ ]] || ! ((value > min)); then
			is_valid=0
			error_msg="请检查参数 [ ${para} ] 的值 ${value} 是否为一个大于 ${min} 的正整数！"
		fi
		;;
	"INT_6")
		# 验证6位正整数
		if ! [[ "$value" =~ ^[1-9][0-9]{5}$ ]]; then
			is_valid=0
			error_msg="请检查参数 [ ${para} ] 的值 ${value} 是否为一个6位数的正整数！"
		fi
		;;
	"INT_R")
		# 验证指定范围内的正整数
		if ! [[ $value =~ ^[1-9][0-9]*$ ]] || ! ((value >= min && value <= max)); then
			is_valid=0
			error_msg="请检查参数 [ ${para} ] 的值 ${value} 是否为一个在 [${min}~${max}] 区间内的正整数！"
		fi
		;;
	*)
		echo "未知的验证类型: $validate_type"
		return 1
		;;
	esac
	if [ $is_valid -eq 0 ]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "$error_msg"
	fi
}
#========================================================
# 工具：command_check
# 逻辑：批量检查系统命令是否存在
#========================================================
function command_check() {
	# 子函数：单条命令存在性检测
	_check_command_existence() {
		local command_name=$1
		if ! command -v "$command_name" >/dev/null 2>&1; then
			ARG_CHECK_FLAG=0
			printf "\n"
			color_printf red "Error: $command_name 命令未找到！"
			printf "\n"
		fi
	}
	# 定义待检查命令列表并循环检测
	local -a commands=("dirname" "mount" "tar")
	for shell_c in "${commands[@]}"; do
		_check_command_existence "$shell_c"
	done
}
#========================================================
# 工具：check_file
# 逻辑：判断文件/目录是否存在
#========================================================
function check_file() {
	[[ -e $1 ]]
}
#========================================================
# 工具：bak_file
# 逻辑：首次备份生成 .original；已修改则备份当前并恢复原始
#========================================================
function bak_file() {
	local src=$1
	local original=${src}.original
	# 源不存在：创建空 .original
	[[ -e $src ]] || {
		run_cmd touch "$original"
		return
	}
	# 已存在 .original：用全局 $current 做时间戳备份，再恢复
	if [[ -e $original ]]; then
		run_cmd "$CP_CMD" -prf "$src"{,."$current"}
		run_cmd "$CP_CMD" -prf "$original" "$src"
		return
	fi
	# 首次备份：不含 "# DMBegin" 时才备份
	if [[ $(run_cmd grep -E -c '# DMBegin' "$src") -eq 0 ]]; then
		run_cmd "$CP_CMD" -prf "$src"{,.original}
	else
		# 含标记，先备份当前，再恢复
		run_cmd "$CP_CMD" -prf "$src"{,."$current"}
		run_cmd "$CP_CMD" -prf "$original" "$src"
	fi
}
#========================================================
# 工具：define_os_flag
# 逻辑：识别操作系统类型、版本及节点角色
#========================================================
function define_os_flag() {
	local or_file=/etc/os-release
	local sr_file=/etc/system-release
	local rr_file=/etc/redhat-release
	local ip_trim
	# 读取 OS 信息
	if [[ -f $or_file ]]; then
		os_type=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' "$or_file")
		pretty_name=$(awk -F= '/^PRETTY_NAME=/{gsub(/"/,"",$2); print $2}' "$or_file")
	else
		os_type=$(awk '{print tolower($1)}' "$([ -f $sr_file ] && echo $sr_file || echo $rr_file)")
		pretty_name=$(cat "$sr_file")
	fi
	# 根据 glibc 主版本号映射到主版本号
	# 定义 os_version
	libc_version=$(ldd --version | head -n 1 | awk '{print $NF}' | cut -d '.' -f 2)
	# 根据 libc_version 获取 os_version
	local -A os_version_map=(
		["6"]="12-16"
		["7"]="17-27"
		["8"]="28-33"
		["9"]="34-38"
		["10"]="39-99" # redhat10 是 ldd (GNU libc) 2.39
	)
	for value in "${!os_version_map[@]}"; do
		IFS="-" read -r start end <<<"${os_version_map[$value]}"
		if ((libc_version >= start && libc_version <= end)); then
			os_version="$value"
			break
		fi
	done
	# 校验是否匹配到 os_version
	if [[ -z "$os_version" ]]; then
		color_printf red "当前操作系统版本 [ $pretty_name ] 不在脚本支持列表中，如有需要请联系开发者适配！"
		exit 1
	fi
	# 根据 OS 类型与版本设置 os_flag
	case ${os_type,,} in
	centos | rhel | ol | rocky | anolis)
		[[ $os_version -ge 8 ]] && os_flag=OS2 || os_flag=OS1
		;;
	uos)
		[[ $(grep -oE '0a|0e' /etc/issue) == "0a" ]] && os_flag=OS2 || os_flag=OS3
		;;
	openeuler) os_flag=OS4 ;;
	*) os_flag=OS1 ;;
	esac
	# 根据 IP 判断节点角色
	for ip in $(hostname -I); do
		ip_trim=$(echo "$ip" | xargs)
		if [[ ${as_pubip_arr[*]} =~ ${ip_trim} ]]; then
			os_flag_value=1
			break
		elif [[ ${sy_pubip_arr[*]} =~ ${ip_trim} ]]; then
			os_flag_value=2
			break
		fi
	done
}
#========================================================
# 工具：install_package
# 逻辑：批量安装 rpm 包，已安装则跳过
#========================================================
function install_package() {
	for pkg in "$@"; do
		# 已安装直接跳过
		rpm -q "$pkg" &>/dev/null && continue
		# 静默安装，失败立即退出
		run_cmd yum install -y -q "$pkg" --skip-broken >/dev/null 2>&1 || {
			printf "\n"
			color_printf red "安装 $pkg 失败，请检查 YUM 源"
			printf "\n"
			exit 1
		}
	done
}
#========================================================
# 工具：install_sshpass
# 逻辑：源码编译安装 sshpass
#========================================================
function install_sshpass() {
	log_print "开始安装 sshpass"
	# 已存在则直接返回
	if command -v sshpass &>/dev/null; then
		printf "\n"
		color_printf green "已存在 sshpass：$(sshpass -V 2>&1 | head -1)"
		printf "\n"
		return 0
	fi
	# 准备编译环境
	command -v gcc &>/dev/null || {
		conf_yum
		install_package gcc
	}
	# 定位源码包
	local src
	src=$(find "$software_dir" -maxdepth 1 -name 'sshpass-*.tar.gz' -print -quit 2>/dev/null)
	[[ -z $src ]] && {
		printf "\n"
		color_printf red "未找到 sshpass-*.tar.gz，请放到 $software_dir"
		printf "\n"
		exit 1
	}
	# 一次性编译安装
	local build_dir="$software_dir/sshpass_build"
	run_cmd mkdir -p "$build_dir"
	cd "$build_dir" || exit 1
	run_cmd tar -xf "$src" --strip-components=1
	run_cmd ./configure --prefix=/usr/local --libdir=/usr/local/libexec
	run_cmd make -s
	run_cmd make install -s
	cd - >/dev/null || exit
	rm -rf "$build_dir"
	# 安装结果验证
	if command -v sshpass &>/dev/null; then
		printf "\n"
		color_printf green "sshpass 安装成功"
		printf "\n"
		exit 1
	else
		printf "\n"
		color_printf red "安装失败，请检查日志"
		printf "\n"
		exit 1
	fi
}
#========================================================
# 工具：exec_ssh_cmd
# 逻辑：通过 ssh 在远程节点执行命令
#========================================================
function exec_ssh_cmd() {
	local node="$1"                                                    # 目标主机
	local cmd="$2"                                                     # 要执行的命令
	local ssh_cmd="ssh -o StrictHostKeyChecking=no -q -p $os_ssh_port" # SSH 命令
	# 若指定密钥，则追加 -i 参数
	if [[ -n "$ssh_key_path" ]]; then
		ssh_cmd="$ssh_cmd -i ${ssh_key_path%/}/id_rsa"
	fi
	# 若启用 sshpass，则追加密码认证
	if [[ "$is_sshpass" =~ [Yy] ]]; then
		ssh_cmd="sshpass -p $root_passwd $ssh_cmd"
	fi
	# 执行命令并捕获退出码
	$ssh_cmd "$node" "$cmd"
	local status=$?
	# 失败时打印错误信息
	if [ $status -ne 0 ]; then
		printf "错误：命令 '%s' 在主机 '%s' 上执行失败，退出码为 %d。\n" "$cmd" "$node" "$status"
		return $status
	fi
}
#========================================================
# 工具：exec_scp_cmd
# 逻辑：通过 scp 上传/下载文件
#========================================================
function exec_scp_cmd() {
	local file_name="$1"
	local node="$2"
	local path="$3"
	local UP_OR_DOWN="$4"
	local scp_cmd="scp -o StrictHostKeyChecking=no -p -q -P $os_ssh_port -r"
	# 若指定密钥，则追加 -i 参数
	if [[ -n "$ssh_key_path" ]]; then
		scp_cmd="$scp_cmd -i ${ssh_key_path%/}/id_rsa"
	fi
	# 若启用 sshpass，则追加密码认证
	if [[ "$is_sshpass" =~ [Yy] ]]; then
		scp_cmd="sshpass -p $root_passwd $scp_cmd"
	fi
	# 根据 UP_OR_DOWN 执行上传或下载
	case "$UP_OR_DOWN" in
	"UP")
		# 上传
		$scp_cmd "$file_name" "$node:$path"
		;;
	"DOWN")
		# 下载
		$scp_cmd "$node:$path" "$file_name"
		;;
	*)
		printf "操作无效。请使用 'UP' 或 'DOWN'。\n"
		return 1
		;;
	esac
}
#==============================================================#
# 使用sftp发送文件到目标服务器
#==============================================================#
function sftp_cmd() {
	local sftp_server_ip=$1
	local sftp_server_port=$2
	local sftp_username=$3
	# shellcheck disable=SC2155
	local sftp_passwd=$(printf "%q" "$4")
	local sftp_path=$5
	local remote_path=$6
	local cmd=$7
	# 确保路径包含空格或特殊字符时不会出错
	sftp_path=$(printf "%q" "$sftp_path")
	remote_path=$(printf "%q" "$remote_path")
	# 公共的 expect 脚本模板
	expect_script="
log_user 0
spawn sftp -o StrictHostKeyChecking=no -P $sftp_server_port $sftp_username@$sftp_server_ip
expect \"password:\"
send \"$sftp_passwd\r\"
expect \"sftp>\"
send \"cd $sftp_path\r\"
expect \"sftp>\"
send \"lcd $remote_path\r\"
expect \"sftp>\"
"
	case "$cmd" in
	"UP")
		# 上传操作
		expect_script+="
send \"mkdir -p dmdir\r\"
expect \"sftp>\"
send \"cd dmdir\r\"
expect \"sftp>\"
send \"rm *\r\"
expect \"sftp>\"
send \"put -r *\r\"
expect \"sftp>\"
send \"quit\r\"
"
		;;
	"DOWN")
		# 下载操作
		expect_script+="
send \"cd dmdir\r\"
expect \"sftp>\"
send \"get -r *\r\"
expect \"sftp>\"
send \"quit\r\"
"
		;;
	*)
		printf "无效的命令！\n"
		exit 1
		;;
	esac
	# 执行 expect 脚本并检查执行结果
	expect -c "$expect_script"
}
#========================================================
# 工具：extract_arg
# 逻辑：从“arg:ip”格式字符串中提取冒号前的参数名
#========================================================
function extract_arg() {
	printf '%s' "${1%%:*}"
}
#========================================================
# 工具：extract_ip
# 逻辑：从“arg:ip”格式字符串中提取冒号后的 IP
#========================================================
function extract_ip() {
	printf '%s' "${1#*:}"
}
#========================================================
# 工具：check_ip_basic
# 逻辑：校验 IP 数组基础规则（首 IP 为本机、无重复）
#========================================================
function check_ip_basic() {
	local -a ip_arr=("$@")
	# 检查第一个 IP 是否为当前主机 IP
	if [[ ${#ip_arr[@]} -gt 0 ]]; then
		local arg ip
		arg=$(extract_arg "${ip_arr[0]}")
		ip=$(extract_ip "${ip_arr[0]}")
		if ! hostname -I | grep -wq "$ip"; then
			printf "\n"
			ARG_CHECK_FLAG=0
			color_printf red "请检查 [ $arg ] 值，输入的第一个IP：$ip 不是当前机器IP，请重新输入！"
		fi
	fi
	# 检测重复 IP
	local dup_ips
	dup_ips=$(printf '%s\n' "${ip_arr[@]}" | awk -F: '{print $2}' | sort | uniq -d)
	if [[ -n "$dup_ips" ]]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "请检查重复的IP地址：$dup_ips，请重新输入！"
	fi
}
#========================================================
# 工具：check_ip
# 逻辑：校验单个 IP 的格式与网络连通性
#========================================================
function check_ip() {
	local arg_ip="$1"
	local arg ip
	arg=$(extract_arg "${arg_ip}")
	ip=$(extract_ip "${arg_ip}")
	# 正则校验 IPv4 格式
	if ! [[ "$ip" =~ ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$ ]]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "请检查参数 [ $arg ] 的 IP 地址 $ip 格式不正确！"
		return
	fi
	# 若启用 ping 检测，则测试网络连通性
	[[ "$is_nec_ping" =~ [Yy] ]] || return
	local current_lang=$LANG
	export LANG="en_US.UTF-8"
	if ! timeout 5 ping -c 2 -w 5 "$ip" &>/dev/null || timeout 5 ping -c 2 -w 5 "$ip" 2>&1 | grep -q "Unreachable"; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "请检查参数 [ $arg ] 的 IP 地址 $ip ，网络不可达！"
	fi
	export LANG="$current_lang"
}
#========================================================
# 工具：add_ips_to_arr
# 逻辑：按安装模式把 IP 写入目标数组并同步生成纯 IP 数组
#========================================================
function add_ips_to_arr() {
	# 子函数：向指定数组追加元素
	_add_ips_to_target() {
		local target_array="$1"
		shift
		for arg_ip in "$@"; do
			local ip
			ip="$(extract_ip "${arg_ip}")"
			if [[ -n "$ip" ]]; then
				eval "$target_array+=(\"$arg_ip\")"
				# 同步维护纯 IP 数组
				[[ "$target_array" == "all_args_pubip_arr" ]] && all_pubip_arr+=("$ip")
				[[ "$target_array" == "all_args_malip_arr" ]] && all_malip_arr+=("$ip")
			fi
		done
	}
	# 按安装模式选择要处理的数组
	case "$dm_install_mode" in
	datawatch)
		_add_ips_to_target all_args_pubip_arr "${db_pubip_arr[@]}" "${as_pubip_arr[@]}" "${sy_pubip_arr[@]}" "-dmoi:${dw_monitor_ip}"
		_add_ips_to_target all_args_malip_arr "${db_malip_arr[@]}" "${as_malip_arr[@]}" "${sy_malip_arr[@]}"
		;;
	raft)
		_add_ips_to_target all_args_pubip_arr "${db_pubip_arr[@]}"
		_add_ips_to_target all_args_malip_arr "${xmal_ip_arr[@]}"
		;;
	*)
		_add_ips_to_target all_args_pubip_arr "${db_pubip_arr[@]}"
		_add_ips_to_target all_args_malip_arr "${db_malip_arr[@]}"
		;;
	esac
}
#========================================================
# 工具：validate_ip
# 逻辑：统一校验公网/内网 IP 数量、格式与连通性
#========================================================
function validate_ip() {
	# 添加ip地址到数组中
	add_ips_to_arr
	# 仅在一节点执行校验
	[[ "$node_num" -eq 1 ]] || return
	# 如果前面参数都正确，再校验IP地址
	[[ $ARG_CHECK_FLAG -eq 1 ]] || return
	log_print "校验IP地址和磁盘"
	# 子函数：统一校验 IP 数组
	_validate_ip_array() {
		local -a ip_arr=("$@")
		local ip_str=""
		for arg_ip in "${ip_arr[@]}"; do
			ip_str+="$(extract_ip "${arg_ip}"),"
		done
		[[ -n "$ip_str" ]] && color_printf white "校验 ${ip_str%,} 地址，请等待！！！"
		check_ip_basic "${ip_arr[@]}"
		for ip in "${ip_arr[@]}"; do
			check_ip "$ip"
		done
	}
	# 分别校验公网与内网 IP
	_validate_ip_array "${all_args_pubip_arr[@]}"
	_validate_ip_array "${all_args_malip_arr[@]}"
}
#========================================================
# 工具：parse_disks_redun
# 逻辑：校验磁盘数量与冗余模式的匹配关系
#========================================================
function parse_disks_redun() {
	# dscm 模式限制 1~5 块磁盘
	IFS=',' read -ra disks <<<"$dcr_disk"
	if [[ ${#disks[@]} -gt 5 ]]; then
		ARG_CHECK_FLAG=0
		printf "\n"
		color_printf red "dscm集群 [-dcd] 参数必须包含 1~5 块磁盘，请重新传递 -dcd 参数！"
	fi
	# 按冗余模式计算最小磁盘数
	local -a disk_redun=(${log_disk:+"$redo_redun"} ${arch_disk:+"$arch_redun"} "$data_redun")
	for ((x = 0; x < ${#disk_redun[@]}; x++)); do
		local min_disks
		case ${disk_redun[$x]} in
		1) min_disks=1 ;;
		2) min_disks=2 ;;
		*) min_disks=3 ;;
		esac
		IFS=',' read -ra disk_count <<<"${disk_arr[$((x + 1))]}"
		if ((${#disk_count[@]} < min_disks)); then
			ARG_CHECK_FLAG=0
			printf "\n"
			color_printf red "磁盘数量 ${disk_count[*]} 不满足冗余模式 ${disk_redun[$x]} 的最小要求！"
		fi
	done
}
#========================================================
# 工具：get_wwid
# 逻辑：获取磁盘或 DM 设备的唯一 WWID/UUID
#========================================================
function get_wwid() {
	local wwid key
	local disk_name=$1
	# 区分块设备路径与 DM 设备名
	if [[ "$disk_name" =~ ^/dev ]]; then
		if [[ $os_version -eq 6 ]]; then
			wwid=$(/sbin/scsi_id -g -u -d "$disk_name")
		else
			if [[ "$disk_name" =~ nvme ]]; then
				[[ "$disk_name" =~ p[0-9]+$ ]] && key="ID_PART_ENTRY_UUID" || key="ID_WWN"
				wwid=$(udevadm info --query=property --name="$disk_name" | awk -F'=' -v key="$key" '$1 == key {print $2}')
			else
				if [[ "$disk_name" =~ [0-9]+$ ]]; then
					wwid=$(udevadm info --query=property --name="$disk_name" | awk -F'=' '$1 == "ID_PART_ENTRY_UUID" {print $2}')
				else
					wwid=$(/usr/lib/udev/scsi_id -g -u -d "$disk_name")
				fi
			fi
		fi
	else
		# DM 设备
		if command -v dmsetup >/dev/null 2>&1; then
			wwid=$(dmsetup info -c | grep "$disk_name" | awk '{print $8}')
		else
			wwid=$(udevadm info --query=property --name="/dev/mapper/${disk_name}" | awk -F'=' '$1 == "DM_UUID" {print $2}')
		fi
	fi
	echo "$wwid"
}
#========================================================
# 工具：check_disk
# 逻辑：校验所有磁盘 WWID 唯一性，可选 dd 清零
#========================================================
function check_disk() {
	# 构造待检查磁盘参数列表
	printf "\n"
	local disk_para
	if [[ $dm_install_mode == "dsc" ]]; then
		mapfile -t disk_para < <(printf "%s\n" "-dcd" "-vod")
	else
		mapfile -t disk_para < <(printf "%s\n" "-dcd")
	fi
	[[ -n $log_disk ]] && disk_para+=("-lod")
	[[ -n $arch_disk ]] && disk_para+=("-ard")
	disk_para+=("-dad")
	local -A disk_para_str=(
		["-dcd"]="$dcr_disk"
		["-vod"]="$vote_disk"
		["-ard"]="$arch_disk"
		["-lod"]="$log_disk"
		["-dad"]="$data_disk"
	)
	local -A disk_uuid_map
	# 遍历每个参数对应的磁盘列表
	for para in "${disk_para[@]}"; do
		IFS=',' read -r -a disks <<<"${disk_para_str[$para]}"
		color_printf white "校验 ${disks[*]} 磁盘，请等待！！！"
		for disk in "${disks[@]}"; do
			local disk_uuid
			disk_uuid=$(get_wwid "$disk")
			# 空 UUID
			if [[ -z "$disk_uuid" ]]; then
				ARG_CHECK_FLAG=0
				printf "\n"
				color_printf red "请检查参数 [ $para ] 的值 $disk 是否正确！"
				printf "\n"
			fi
			# 重复 UUID
			if [[ -n "${disk_uuid_map[$disk_uuid]}" ]]; then
				ARG_CHECK_FLAG=0
				printf "\n"
				color_printf red "在参数 [ $para ] 中检测到重复的磁盘：$disk"
				printf "\n"
			fi
			disk_uuid_map["$disk_uuid"]="$disk"
			# 可选：dd 清零前 1 GB
			if [[ $format_disk == "Y" ]]; then
				dd if=/dev/zero of="$disk" bs=100M count=10 >/dev/null 2>&1 &
				wait
			fi
		done
	done
	# 优化日志输出格式
	[[ $ARG_CHECK_FLAG -eq 0 ]] && printf "\n"
}
#========================================================
# 工具：validate_disk
# 逻辑：在 dsc/dscm 模式下生成磁盘与 ASM 磁盘数组
#========================================================
function validate_disk() {
	# 前面参数都正确，继续校验磁盘
	[[ $ARG_CHECK_FLAG -eq 1 ]] || return
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
		# 构造磁盘数组，${log_disk:+$log_disk}和${arch_disk:+$arch_disk}不要加双引号，否则数组元素会错乱
		if [[ $dm_install_mode == "dsc" ]]; then
			mapfile -t disk_arr < <(printf "%s\n" "$dcr_disk" "$vote_disk" ${log_disk:+"$log_disk"} ${arch_disk:+"$arch_disk"} "$data_disk")
			mapfile -t asm_disk_arr < <(printf "%s\n" "dmdcr" "dmvote" ${log_disk:+"dmlog"} ${arch_disk:+"dmarch"} "dmdata")
		else
			mapfile -t disk_arr < <(printf "%s\n" "$dcr_disk" ${log_disk:+"$log_disk"} ${arch_disk:+"$arch_disk"} "$data_disk")
			mapfile -t asm_disk_arr < <(printf "%s\n" "dmdcrv" ${log_disk:+"dmlog"} ${arch_disk:+"dmarch"} "dmdata")
			# 校验磁盘数量与冗余模式的匹配关系
			parse_disks_redun
		fi
		# 仅在首节点执行磁盘校验
		[[ $node_num -eq 1 && -n "$data_disk" ]] && check_disk
	fi
}
#========================================================
# 工具：filter_disk
# 逻辑：列出系统所有非过滤磁盘的 WWID、名称、容量
#========================================================
function filter_disk() {
	local fil_disk=$1
	local -A wwids sizes
	local disk_list=()
	# 子函数：获取磁盘容量（GB）
	_disk_storage() {
		lsblk -b -o SIZE,TYPE "${1}" | awk '$2 == "disk" {print $1/1024/1024/1024 "G"}'
	}
	# 列出所有 sd/vd 开头的磁盘
	local all_disks
	all_disks=$(lsblk -n -o NAME | awk '/^sd|vd/ { print $1 }')
	# 排除过滤列表中的磁盘
	IFS=',' read -ra fil_disk_arr <<<"$fil_disk"
	for disk in $all_disks; do
		if ! [[ "${fil_disk_arr[*]}" =~ $disk ]]; then
			disk_list+=("/dev/$disk")
		fi
	done
	# 收集 WWID 与容量
	for disk in "${disk_list[@]}"; do
		sizes[$disk]=$(_disk_storage "$disk")
		local wwid
		wwid=$(get_wwid "$disk")
		[[ -n $wwid && ! "${wwids[*]}" =~ $wwid ]] && wwids[$disk]=$wwid
	done
	# 打印结果（按容量升序，容量相同按名称）
	printf "\n"
	color_printf light_blue "Disk WWID" "Disk Name" "Size"
	printf "\n"
	for disk in "${!wwids[@]}"; do
		color_printf white "${wwids[$disk]}" "$disk" "${sizes[$disk]}"
	done | sort -k3,3n -k2,2
	printf "\n"
}
#========================================================
# 工具：execute_sql
# 逻辑：以 dmdba 身份执行单条 SQL 命令
#========================================================
function execute_sql() {
	local user_name="$1"
	local user_pwd="$2"
	local sql_command="$3"
	# 打印待执行的 SQL（便于调试）
	printf "执行 SQL 命令: %s\n" "$sql_command"
	# 调用 disql 执行，静默模式 -S -L
	run_cmd su - dmdba -c "${env_dm_home}/bin/disql -S -L ${user_name}/'\"$user_pwd\"':$port_num -c \"set heading off\" -e \"$sql_command\""
	# 将 disql 的退出码原样返回
	return $?
}
#========================================================
# 工具：execute_script
# 逻辑：以 dmdba 身份执行 SQL 脚本文件
#========================================================
function execute_script() {
	local user_name="$1"
	local user_pwd="$2"
	local script_name="$3"
	# 确认脚本文件存在
	if [[ ! -f "$scripts_dir/$script_name" ]]; then
		printf "错误: 脚本文件 %s/%s 不存在\n" "$scripts_dir" "$script_name"
		exit 1
	fi
	# 通过 disql 的 start 命令执行脚本
	run_cmd su - dmdba -c "${env_dm_home}/bin/disql -S -L ${user_name}/'\"$user_pwd\"':$port_num" <<EOF
start $scripts_dir/$script_name
EOF
	# 返回 disql 的退出码
	return $?
}
#========================================================
# 工具：check_db_status
# 逻辑：轮询数据库实例状态，直到达到目标状态
#========================================================
function check_db_status() {
	local target_status="$1"
	local WAIT_TIME="1s"
	while true; do
		# 查询 v$instance.status$
		status=$(execute_sql "SYSDBA" "$sysdba_pwd_temp" "select status\\\$ from v\\\$instance;" | tail -n 1)
		# 匹配则退出循环，否则等待
		if [[ $status == "$target_status" ]]; then
			break
		else
			sleep "$WAIT_TIME"
		fi
	done
}
#========================================================
# 工具：ssh_check
# 逻辑：检测与目标 IP 的 SSH 免密互信是否已建立
#========================================================
function ssh_check() {
	local username="$1"
	shift
	# 优先使用 expect（若已安装）
	local use_expect=false
	command -v expect >/dev/null 2>&1 && use_expect=true
	# 子函数：单 IP SSH 连通性测试
	_check_ssh() {
		local ip="$1"
		if $use_expect; then
			expect -c "
set timeout 30
spawn ssh -o StrictHostKeyChecking=no -o BatchMode=yes -p $os_ssh_port ${username}@${ip} exit
expect {
  \"denied\"   { exit 1 }
  \"timed out\" { exit 1 }
  eof          { exit 0 }
}
expect eof
" >/dev/null 2>&1
		else
			ssh -q -o ConnectTimeout=30 -o ConnectionAttempts=5 \
				-o PreferredAuthentications=publickey \
				-o StrictHostKeyChecking=no -p "$os_ssh_port" \
				"${username}@${ip}" exit >/dev/null 2>&1
		fi
		return $?
	}
	# 遍历所有 IP，任一失败即返回 false
	for ip in "$@"; do
		if ! _check_ssh "$ip"; then
			echo "false"
			return 1
		fi
	done
	echo "true"
}
#========================================================
# 工具：ssh_trust
# 逻辑：批量建立 SSH 免密互信
#========================================================
function ssh_trust() {
	local dest_user="$1"
	local passwd
	passwd=$(printf "%q" "$2")
	local key_copy_method="$3" # 1=ssh-copy-id  2=manual
	shift 3
	umask 077
	# 确定 SSH 目录路径
	local ssh_dir
	ssh_dir=$([[ $dest_user == "root" ]] && echo "/root/.ssh" || echo "/home/$dest_user/.ssh")
	# 备份已存在的 .ssh 目录
	if check_file "$ssh_dir"; then
		/bin/mv -f "$ssh_dir" "$ssh_dir.$current"
	fi
	# 创建 .ssh 目录并生成密钥
	/bin/mkdir -p "$ssh_dir" && chmod 700 "$ssh_dir"
	local key_path="$ssh_dir/id_rsa"
	ssh-keygen -t rsa -f "$key_path" -P '' -q
	# 子函数：expect 通用模板
	_expect_script() {
		local action="$1"
		expect -c "
set timeout 30
log_user 0
spawn $action
expect {
  \"password:\" { send \"$passwd\r\"; exp_continue }
  \"denied\"    { puts \"连接失败，请检查用户名和密码。\"; exit 1 }
  \"added:\"    { puts \"目标主机 $ip 互信配置成功。\"; exit 0 }
  \"timed out\" { puts \"操作超时，请检查网络连接或目标主机是否可达。\"; exit 1 }
  eof           { exit 0 }
}
expect eof
"
	}
	# 按指定方式分发公钥
	if [[ "$key_copy_method" -eq 1 ]]; then
		for ip in "$@"; do
			_expect_script "ssh-copy-id -f -o StrictHostKeyChecking=no -p $os_ssh_port $dest_user@$ip"
		done
	elif [[ "$key_copy_method" -eq 2 ]]; then
		cat "${key_path}.pub" >>"$ssh_dir/authorized_keys" && chmod 600 "$ssh_dir/authorized_keys"
		for ip in "$@"; do
			_expect_script "ssh -q -o StrictHostKeyChecking=no -p $os_ssh_port $dest_user@$ip \"echo 'added:'\""
		done
	else
		echo "无效的公钥复制方式: $key_copy_method"
		return 1
	fi
	# 并行分发 .ssh 目录到其余节点
	for ip in "${@:2}"; do
		_expect_script "scp -q -o StrictHostKeyChecking=no -P $os_ssh_port -r $ssh_dir $dest_user@$ip:~"
	done
	# OS6 恢复 SELinux 上下文
	if [[ $os_version -eq 6 ]]; then
		for ip in "$@"; do
			_expect_script "ssh -q -o StrictHostKeyChecking=no -p $os_ssh_port $dest_user@$ip \"/sbin/restorecon -RF $ssh_dir\""
		done
	fi
}
#========================================================
# 工具：gen_password
# 逻辑：生成或校验 12 位高强度随机密码
#========================================================
function gen_password() {
	local password=${1:-}
	local flag=${2:-}
	# 子函数：校验密码复杂度
	_check_password() {
		local password=$1
		local -a rules_sort=("A" "B" "C" "D" "E")
		local -A rules=(
			["A"]="^.{9,}$" ["A_DESC"]="密码长度至少为9位"
			["B"]="[A-Z]" ["B_DESC"]="密码必须包含至少一个大写字母"
			["C"]="[a-z]" ["C_DESC"]="密码必须包含至少一个小写字母"
			["D"]="[0-9]" ["D_DESC"]="密码必须包含至少一个数字"
			["E"]="[^[:alnum:]]" ["E_DESC"]="密码必须包含至少一个特殊字符"
		)
		local error_messages=""
		for rs in "${rules_sort[@]}"; do
			if ! [[ $password =~ ${rules[$rs]} ]]; then
				error_messages+="${rules[${rs}_DESC]},"
			fi
		done
		if [[ -n $error_messages ]]; then
			ARG_CHECK_FLAG=0
			printf "\n"
			color_printf red "请检查参数[ $flag ] ,密码不符合要求（长度需要9位以上，包含大小写字母，数字，特殊符号）：${error_messages%,}"
		fi
	}
	# 子函数：生成随机密码
	_generate_password() {
		export LC_ALL=C
		shuf -e \
			"$(tr -dc '[:upper:]' </dev/urandom | head -c 3)" \
			"$(tr -dc '[:lower:]' </dev/urandom | head -c 3)" \
			"$(tr -dc '0-9' </dev/urandom | head -c 4)" \
			"$(tr -dc '@#%&*_+=' </dev/urandom | head -c 2)" |
			tr -d ' ' | fold -w1 | shuf | tr -d '\n'
	}
	# 无入参则生成，有入参则校验
	if [[ -z $password ]]; then
		_generate_password
	else
		_check_password "$password"
	fi
}
#========================================================
# 工具：upper
# 逻辑：将输入字符串转为大写
#========================================================
function upper() {
	echo "${1^^}"
}
#========================================================
# 工具：lower
# 逻辑：将输入字符串转为小写
#========================================================
function lower() {
	echo "${1,,}"
}
#========================================================
# 工具：escape_string
# 逻辑：对字符串进行 JS 转义，避免 JSON 注入
#========================================================
function escape_string() {
	local input="$1"
	printf '%s' "$input" |
		sed \
			-e 's/\\/\\\\/g' \
			-e 's/"/\\"/g' \
			-e 's/\//\\\//g' \
			-e 's/\x08/\\b/g' \
			-e 's/\x0C/\\f/g' \
			-e 's/\x0A/\\n/g' \
			-e 's/\x0D/\\r/g' \
			-e 's/\x09/\\t/g' \
			-e 's/\$/\\$/g'
}
#========================================================
# 工具：recreate_dir
# 逻辑：远程卸载 ISO 并重建安装目录
#========================================================
function recreate_dir() {
	local target_ip="$1"
	# 导出 run_cmd 供远程 shell 使用
	export -f run_cmd
	exec_ssh_cmd "$target_ip" "$(declare -f run_cmd)
while IFS= read -r line; do
  if [[ \$line =~ dm8|dm9 && \$line =~ iso9660 ]]; then
    device=\$(echo \"\$line\" | awk '{print \$(NF-3)}')
    run_cmd umount -f \"\$device\"
  fi
done <<<\"\$(run_cmd mount)\"
[[ -e ${software_dir} || -d ${software_dir} ]] && run_cmd rm -rf ${software_dir}
mkdir -p -m 755 ${software_dir}"
}
#========================================================
# 工具：db_service_cmd
# 逻辑：根据安装模式生成数据库服务启停命令
#========================================================
function db_service_cmd() {
	# 确定服务脚本路径
	if [[ $os_version -eq 6 ]]; then
		local db_service_path=/etc/rc.d/init.d
	else
		local db_service_path=$env_dm_home/bin
	fi
	# 按安装模式生成对应命令
	case $dm_install_mode in
	single | datawatch | raft)
		DMW_START_CMD="$db_service_path/DmWatcherService${dw_group_name} start"
		DMS_START_CMD="$db_service_path/DmService${db_name} start"
		DMM_START_CMD="$db_service_path/DmMonitorService$dw_group_name start"
		DMW_STOP_CMD="$db_service_path/DmWatcherService${dw_group_name} stop"
		DMS_STOP_CMD="$db_service_path/DmService${db_name} stop"
		DMM_STOP_CMD="$db_service_path/DmMonitorService$dw_group_name stop"
		DMS_RESTART_CMD="$db_service_path/DmService${db_name} restart"
		;;
	dsc | dscm)
		DM_CSS_SERVICE="DmCSSServiceCss"
		if [[ $dm_install_mode == "dsc" ]]; then
			DM_ASM_SERVICE="DmASMSvrServiceAsmsvr"
		else
			DM_ASM_SERVICE="DmASMSvrmServiceAsmsvr"
		fi
		DM_DB_SERVICE="DmService${dsc_db_name}"
		CSS_START_CMD="$db_service_path/DmCSSServiceCss start"
		ASMSVR_START_CMD="$db_service_path/$DM_ASM_SERVICE start"
		DB_START_CMD="$db_service_path/$DM_DB_SERVICE start"
		DMC_STOP_CMD="$db_service_path/DmCSSServiceCss stop"
		;;
	esac
}
#========================================================
# 工具：uninstall_service
# 逻辑：根据OS版本卸载服务
#========================================================
function uninstall_service() {
	_uninstall_svc() {
		local service_name=$1
		local service_path
		local uninstall_cmd
		if ((os_version == 6)); then
			service_path="/etc/init.d/${service_name}"
			uninstall_cmd="run_cmd mv -f ${service_path} ${service_path}${current} >/dev/null 2>&1"
		else
			service_path="${env_dm_home}/bin/${service_name}"
			uninstall_cmd="echo 'y' | run_cmd ${env_dm_home}/script/root/dm_service_uninstaller.sh -n ${service_name} >/dev/null 2>&1"
		fi
		check_file "${service_path}" && eval "$uninstall_cmd"
	}
	# 按照模式卸载可能存在的服务
	case $dm_install_mode in
	single | raft)
		_uninstall_svc "DmService${db_name}"
		;;
	datawatch)
		if ((node_num == -1)); then
			_uninstall_svc "DmMonitorService${dw_group_name}"
		else
			_uninstall_svc "DmService${db_name}"
			_uninstall_svc "DmWatcherService${dw_group_name}"
		fi
		;;
	dsc | dscm)
		local css_service="$DM_CSS_SERVICE"
		local asm_service="$DM_ASM_SERVICE"
		local db_service="DmService${dsc_db_name}"
		for svc in "$css_service" "$asm_service" "$db_service"; do
			_uninstall_svc "${svc}"
		done
		;;
	esac
}
#========================================================
# 函数：cleanup_on_exit
# 作用：恢复口令策略和清理临时目录
#========================================================
function cleanup_on_exit() {
	# 1. 恢复配置文件
	run_cmd "$CP_CMD" -prf "${pwq_conf}.original" "${pwq_conf}"
	# 2. 如果设置了 dm_iso_name，则执行卸载和清理
	if [[ -n $dm_iso_name ]]; then
		run_cmd umount -f "$mount_dir" 2>/dev/null
		run_cmd find "$software_dir" -type d -empty -delete 2>/dev/null
	fi
	# 3.删除临时生成的授权文件
	local temp_dir
	temp_dir=$(mktemp -d)
	[[ -f "${software_dir}/dm${oguid}.key" ]] && run_cmd mv "${software_dir}"/dm"${oguid}".key "${temp_dir}"
}
#========================================================
# 帮助
#========================================================
function print_options() {
	local options=("$@")
	# 调用 color_printf 函数，输出绿色字体
	# ${option%% *} 表示从 option 变量中删除最后一个空格及其后面的字符，保留前面的部分
	# ${option#* } 表示从 option 变量中删除第一个空格及其前面的字符，保留后面的部分
	for option in "${options[@]}"; do
		color_printf green "${option%% *}" "${option#* }"
	done
}
#========================================================
# 普通帮助
#========================================================
function help() {
	declare -a options=()
	printf "\n"
	color_printf blue "用途: 一键部署达梦数据库[单机/数据守护[raft主备]/DMDSC集群]"
	printf "\n"
	color_printf red "用法:./$(basename "$0") [选项] 选项值 { COMMAND | help }"
	printf "\n"
	color_printf blue "单机(Single): "
	printf "\n"
	options=(
		"-di 达梦数据库安装镜像名称"
		"-kp 达梦数据库密钥路径"
		"-hn 主机名"
		"-dp 系统用户dmdba密码，默认值：[Dameng@123]"
		"-d 数据库软件安装目录，默认值：[/home/dmdba/dmdbms]"
		"-dd 数据库文件目录，默认值：[/dmdata]"
		"-ad 数据库归档目录，默认值：[/dmarch]"
		"-bd 数据库备份目录，默认值：[/dmbak]"
		"-cd 数据库CORE目录，默认值：[/dmbak/dmcore]"
		"-dn 数据库名称，默认值：[DAMENG]"
		"-in 数据库实例名称，默认值：[DMSERVER]"
		"-es 数据文件簇大小，默认值：[32]"
		"-ps 数据页大小，默认值：[32]"
		"-cs 字符串大小写敏感，默认值：[Y]"
		"-c 数据库字符集，默认值：[1]"
		"-cm 是否兼容其他数据库模式，取值：0~7，默认值：[0]"
		"-bpm 设置结尾空格填充模式，当等于1时，默认兼容ORACLE，即[-cm 2]，默认值：[0]"
		"-osa 是否开启os认证登录数据库，默认值：[Y]"
		"-ls 日志文件大小，单位M，默认值：[1024]"
		"-rpn 日志文件后缀名，长度不超过10。默认值：[rlog]"
		"-er 是否开启归档模式，默认值：[Y]"
		"-sl 归档空间大小，单位M，默认值：[102400]"
		"-pn 监听端口号，默认值：[5236]"
		"-sp 数据库用户SYSDBA密码：(1)指定密码则包含大小写字符、数字、特殊符号、大于9位数的密码，(2)不传 -sp 则随机生成12位数密码(建议使用)"
		"-sap 数据库用户SYSAUDITOR密码：(1)指定密码则包含大小写字符、数字、特殊符号、大于9位数的密码，(2)不传 -sap 则随机生成12位数密码(建议使用)"
		"-ssp 数据库用户SYSSSO密码：(1)指定密码则包含大小写字符、数字、特殊符号、大于9位数的密码，(2)不传 -ssp 则随机生成12位数密码(建议使用)"
		"-bm 数据库备份模式，模式[1]：每天全备，模式[2]：周六全备，周日到周五增备[2]，默认值：[2]"
		"-mp 优化数据库时物理内存占比，默认值：[80]"
		"-om 并发量较高的OLTP数据库参数 [1]，并发量不高的一般业务和OLAP类的数据库参数参数值 [0]，默认值：[0]"
		"-m 仅配置操作系统，默认值：[N]"
		"-ud 仅安装达梦数据库软件，默认值：[N]"
		"-oid 仅初始化数据库，默认值：[N]"
		"-opd 自动优化数据库，默认值：[Y]"
		"-iso 部署集群或时间服务器，需要挂载ISO镜像，脚本自动配置 YUM 源，默认值：[Y]"
		"-ti 时间服务器IP地址[需要配置YUM源]"
		"-hf 安装完成是否配置内存大页，默认值：[N]"
	)
	print_options "${options[@]}"
	printf "\n"
	color_printf blue "主备集群参数(DataWatch && raft主备): "
	printf "\n"
	options=(
		"-inp 是否ping服务器IP地址，校验连通性，默认值：[Y]"
		"-osp ssh端口，默认值：[22]"
		"-ois 仅安装sshpass(每个节点都需要安装)，默认值：[N]"
		"-isp 是否使用sshpass模式，默认值：[N]"
		"-rp 执行脚本的用户密码，当前用户是：$(whoami)"
		"-skp 使用现有的互信(互信必须是双向免密)，指定互信配置文件所在目录，例如：/$(whoami)/.ssh"
		"-hn 主机名前缀，配置每个节点主机名为dw01,dw02...，例如：-hn dw"
		"DataWatch参数： ------------------------------------------------------------"
		"-dpi DW 实[即]时主备公网IP，异[同]步主库公网IP，如果是实[即]时主备公网IP，以逗号隔开，例如：-dpi 192.168.31.181,192.168.31.182"
		"-dmi DW 实[即]时主备私网IP，异[同]步主库私网IP，如果是实[即]时主备公网IP，以逗号隔开，例如：-dmi 1.1.1.181,1.1.1.182"
		"-api DW 异步备库公网IP，以逗号隔开，例如：-api 192.168.31.183"
		"-ami DW 异步备库私网IP，以逗号隔开，例如：-dmi 1.1.1.183"
		"-spi DW 同步备库公网IP，以逗号隔开，例如：-spi 192.168.31.184"
		"-smi DW 同步备库私网IP，以逗号隔开，例如：-dmi 1.1.1.184"
		"-dmoi 监视器主机IP，例如：-dmoi 192.168.31.185"
		"-dgn 数据守护组名，默认值：[GRP1]"
		"-mpn 私网监听端口号，取值：1024~65535，默认值：[5336]"
		"-mdpn 守护进程端口号，取值：1024~65535，默认值：[5436]"
		"-midpn 实例监听守护进程端口号，取值：1024~65535，默认值：[5536]"
		"-at 数据守护归档模式，取值：0、1，其中REALTIME[0]，TIMELY[1]，默认值：[0]"
		"-awa 数据守护性能模式，取值：0、1，其中高性能模式[0]，事务一致性模式[1]，默认值：[1]"
		"-ri 主库向异步备库发送归档时间间隔，取值：0~60秒，默认值：[60]"
		"-art 同步备库异步恢复的时间间隔，单位秒，取值范围：1~86400，默认值：[1]"
		"-sfi sftp服务器IP，例如：-sfi 192.168.31.186"
		"-sfo sftp服务器端口，默认值：[22]"
		"-sfu sftp服务器用户名"
		"-sfp sftp服务器密码"
		"-sfd sftp服务器根目录，例如：/home/sftpuser/uploads"
		"raft主备参数：  ------------------------------------------------------------"
		"-dpi raft主备节点公网IP，以逗号隔开，例如：-dpi 192.168.31.181,192.168.31.182,192.168.31.183"
		"-xi raft主备节点私网IP[XMAL IP地址]，以逗号隔开，例如：-xi 1.1.1.181,1.1.1.182,192.168.31.183"
		"-xp raft主备节点私网监听端口号[XMAL监听端口]，取值：1024~65535，默认值：[10006]"
		"-rhi raft组内实例通信心跳校验间隔，取值范围：5~1800毫秒，默认值：150"
		"-rvi raft组内实例基础投票间隔，取值范围：10~60000毫秒，默认值：3000"
		"-xhi xmal通信心跳校验间隔，取值范围： 1~600秒，默认值：5"
		"-adpt 归档目标监听端口号，取值：1024~65535，默认值：[10006]"
	)
	print_options "${options[@]}"
	printf "\n"
	color_printf blue "DSC集群(DMDSC): "
	printf "\n"
	options=(
		"-inp 是否ping服务器IP地址，校验连通性，默认值：[Y]"
		"-osp ssh端口，默认值：[22]"
		"-ois 仅安装sshpass(每个节点都需要安装)，默认值：[N]"
		"-isp 是否使用sshpass模式，默认值：[N]"
		"-rp 执行脚本的用户密码，当前用户是：$(whoami)"
		"-skp 使用现有的互信(互信必须是双向免密)，指定互信配置文件所在目录，例如：/$(whoami)/.ssh"
		"-hn 主机名前缀，配置每个节点主机名为dsc01,dsc02...，例如：-hn dsc"
		"-dpi DSC所有节点公网IP，以逗号隔开，例如：-dpi 192.168.31.181,192.168.31.182"
		"-dmi DSC所有节点私网IP，以逗号隔开，例如：-dmi 1.1.1.181,1.1.1.182"
		"-lci DSC第三方确认公网IP，例如：-lci 192.168.31.185"
		"-cdp CSS公网通信端口，取值：1024~65534，默认值：[9341]"
		"-adp ASM公网通信端口，取值：1024~65534，默认值：[9351]"
		"-ddp DB 公网通信端口，取值：1024~65534，默认值：[9361]"
		"-amp ASM私网通信端口，取值：1024~65534，默认值：[9451]"
		"-dmp DB 私网通信端口，取值：1024~65534，默认值：[9461]"
		"-dcd DCR[V]磁盘列表表，DSC集群只能传入一块磁盘，例如：/dev/sdb，DSCM集群时，可以传 1~5 块磁盘，例如：/dev/sdb,/dev/sdc,/dev/sdd"
		"-vod VOTE磁盘列表，DSC集群只能传入一块磁盘，例如：/dev/sdc，DSCM集群时，不用传此参数"
		"-lod REDO磁盘列表，可以是一块盘，也可以是多块盘，如果没有redo磁盘，可以不写，例如：/dev/sdd,/dev/sde"
		"-ard ARCH磁盘列表，可以是一块盘，也可以是多块盘，如果没有arch磁盘，可以不写，例如：/dev/sdf,/dev/sdg"
		"-dad DATA磁盘列表，可以是一块盘，也可以是多块盘，盘数必须大于等于 1        ，例如：/dev/sdh,/dev/sdi"
		"-rr REDO镜像文件冗余模式(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1]"
		"-ar ARCH镜像文件冗余模式(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1]"
		"-dr DATA镜像文件冗余模式(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1]"
		"-lgm REDO文件副本数(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1] "
		"-aam ARCH文件副本数(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1] "
		"-dtm SYSTEM/MAIN/ROLL 表空间数据文件副本数(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1] "
		"-ctm dm.ctl 和 dm_service.prikey文件副本数(ASM镜像独有参数)，取值：1、2 或 3；默认值：[1] "
		"-lst REDO日志条带化粒度(ASM镜像独有参数)，取值：0、32、64、128、256，单位 KB。默认值：[64]"
		"-aas ARCH日志条带化粒度(ASM镜像独有参数)，取值：0、32、64、128、256，单位 KB。默认值：[64]"
		"-dst DATA文件条带化粒度(ASM镜像独有参数)，取值：0、32、64、128、256，单位 KB。默认值：[32]"
		"-as 数据分配单元(ASM镜像独有参数)，取值： 1、2、4、8、16、32、64，单位 BYTES。默认值：[4]"
		"-rs ASM磁盘组日志文件大小(ASM镜像独有参数)，取值 0、32、64、128、256，单位 MB。默认值：[128]"
		"-ila 是否配置本地归档，如果配置，默认数据库归档目录 [/dmarch]，可以由参数-ad指定具体目录，默认值：[N]"
		"-fld 过滤重复磁盘，保留输出唯一盘符，参数值为非ASM盘符(系统盘等)，例如：-fld sda，多个盘符用逗号拼接：-fld sda,sdb"
		"-fmd 是否需要格式化共享存储盘，默认值：[Y]"
		"-mtp 是否需要配置multipath多链路聚合，默认值：[N]"
		"-ddn 数据库DB_NAME，默认值：[DSC]"
		"-den 数据库每个节点的实例名前缀，默认值：[DSC]"
		"-apd ASM实例密码，默认值：[Dm@ASM12321]"
	)
	print_options "${options[@]}"
	printf "\n"
	exit 0
}
#========================================================
# 隐藏帮助
#========================================================
function hidden_help() {
	declare -a options=()
	printf "\n"
	color_printf blue "服务器(OS): "
	printf "\n"
	options=(
		"-dbg 打开脚本调试模式，默认值：[N]"
		"-ocu 仅创建具有 (ALL) NOPASSWD: ALL 权限的 dmdba 操作系统用户，默认值：[N]"
	)
	print_options "${options[@]}"
	printf "\n"
	color_printf blue "数据库(DB): "
	printf "\n"
	options=(
		"-lic VARCHAR 类型对象的长度是否以字符为单位，默认值：[N]"
		"-dpv 初始化数据库特殊参数(参数值必须用单引号括起来)，例如：-dpv 'PAGE_CHECK=2 PAGE_HASH_NAME=SHA256'"
		"-oguid 部署数据守护集群或者DSC集群时，生成6位数随机正整数，且首位不是0，如果传参，则是参数值。"
		"-ari 部署DSC集群时候，使用别名代替心跳网络IP地址，默认值：[N] "
	)
	print_options "${options[@]}"
	printf "\n"
	exit 0
}
#========================================================
# 工具：_print_cpu_info
# 逻辑：一次性读取 /proc/cpuinfo，提取并打印关键 CPU 信息
#========================================================
function _print_cpu_info() {
	# 切换英文环境，避免本地化干扰
	local current_lang=$LANG
	export LANG="en_US.UTF-8"
	# 一次性缓存 /proc/cpuinfo，减少多次读取
	local cpuinfo_temp
	cpuinfo_temp=$(mktemp)
	cat /proc/cpuinfo >"$cpuinfo_temp"
	# 定义输出顺序与描述
	local -a keys=("A" "B" "C" "D")
	local -A keywords=(
		["A"]="$(grep "model name" "$cpuinfo_temp" | head -n 1 | awk -F ': ' '{print $2}')"
		["A_DESC"]="型号名称                "
		["B"]="$(grep "physical id" "$cpuinfo_temp" | sort -u | wc -l)"
		["B_DESC"]="物理 CPU 个数           "
		["C"]="$(grep "core id" "$cpuinfo_temp" | sort -u | wc -l)"
		["C_DESC"]="每个物理 CPU 的逻辑核数 "
		["D"]="$(grep -c "processor" "$cpuinfo_temp")"
		["D_DESC"]="系统的 CPU 线程数       "
	)
	# 按固定顺序输出
	for key in "${keys[@]}"; do
		printf "%s: %s\n" "${keywords[${key}_DESC]}" "${keywords[$key]}"
	done
	# 恢复原始语言环境
	export LANG="$current_lang"
}
#========================================================
# 工具：_get_os_version
# 逻辑：跨发行版获取操作系统完整名称
#========================================================
function _get_os_version() {
	local files=(/etc/os-release /etc/system-release /etc/redhat-release)
	local file
	for file in "${files[@]}"; do
		[[ -f $file ]] || continue
		if [[ $file == "/etc/os-release" ]]; then
			# PRETTY_NAME="CentOS Linux 7 (Core)" -> CentOS Linux 7 (Core)
			sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$file" && return
		else
			# 其他文件直接取首行
			head -1 "$file" && return
		fi
	done
	echo "Unknown OS"
}
#========================================================
# 工具：_parse_memory_info
# 逻辑：解析 free -m 输出，返回总内存与使用概况
#========================================================
function _parse_memory_info() {
	local mem_info_m=$1
	local mem_line
	mem_line=$(grep -E '^Mem:' <<<"$mem_info_m")
	if [[ -n $mem_line ]]; then
		# shellcheck disable=SC2034
		read -r _ mem_total mem_used mem_free <<<"$mem_line"
		local mem_total_h mem_usage
		mem_total_h=$(awk '/^Mem:/ {print $2}' <<<"$mem_info_m")
		mem_usage="总:${mem_total_h}, 已用:$(awk '/^Mem:/ {print $3}' <<<"$mem_info_m"), 空闲:$(awk '/^Mem:/ {print $4}' <<<"$mem_info_m")"
		echo "$mem_total_h"
		echo "$mem_usage"
	else
		printf "\n"
		echo "无法获取内存信息"
	fi
}
#========================================================
# 工具：print_sysinfo
# 逻辑：并行收集并打印系统核心信息
#========================================================
function print_sysinfo() {
	log_print "打印系统信息"
	# 创建临时目录，避免并发写冲突
	local temp_dir
	temp_dir=$(mktemp -d)
	# 定义所有需要执行的系统探测命令
	local system_info_commands=(
		"uname -r >'$temp_dir/kernel_version'"
		"uname -m >'$temp_dir/kernel_arch'"
		"virt-what 2>/dev/null >'$temp_dir/virt_output'"
		"ldd --version 2>/dev/null | head -1 | awk '{print \$NF}' >'$temp_dir/glibc_ver'"
		"cat /proc/version >'$temp_dir/proc_version'"
	)
	local memory_info_commands=(
		"free -h >'$temp_dir/free_h'"
		"free -m >'$temp_dir/free_m'"
	)
	local network_disk_info_commands=(
		"ip -4 a >'$temp_dir/ip_output'"
		"df -Th >'$temp_dir/df_output'"
		"grep -Ev '^#|^\\*|^$' /etc/fstab 2>/dev/null >'$temp_dir/fstab_output'"
	)
	# 一次性并行执行所有命令
	local all_commands=("${system_info_commands[@]}" "${memory_info_commands[@]}" "${network_disk_info_commands[@]}")
	for cmd in "${all_commands[@]}"; do
		eval "$cmd" &
	done
	wait
	# 读取临时文件到变量
	local mem_info_m net_info fs_info kernel_ver kernel_arch virt_info glibc_ver proc_ver fstab_info
	mem_info_m=$(cat "$temp_dir/free_m")
	net_info=$(cat "$temp_dir/ip_output")
	fs_info=$(cat "$temp_dir/df_output")
	kernel_ver=$(cat "$temp_dir/kernel_version")
	kernel_arch=$(cat "$temp_dir/kernel_arch")
	virt_info=$(cat "$temp_dir/virt_output")
	glibc_ver=$(cat "$temp_dir/glibc_ver")
	proc_ver=$(cat "$temp_dir/proc_version")
	fstab_info=$(cat "$temp_dir/fstab_output")
	# 填充全局 sysinfo 关联数组
	if [[ $dm_install_mode == "single" ]]; then
		sysinfo["server_ip"]=$(hostname -I | awk '{print $1}')
	else
		local idx=$((node_num == -1 ? ${#all_pubip_arr[@]} - 1 : node_num - 1))
		sysinfo["server_ip"]="${all_pubip_arr[$idx]}"
	fi
	read -r sysinfo["mem_total_h"] sysinfo["mem_usage"] <<<"$(_parse_memory_info "$mem_info_m")"
	sysinfo["net_info"]="$(escape_string "${net_info}")"
	sysinfo["filesystem"]="$(escape_string "${fs_info}")"
	sysinfo["os_version"]=$(_get_os_version)
	sysinfo["hostname"]=$(hostname)
	sysinfo["cpu_count"]=$(nproc)
	sysinfo["current_time"]=$(date)
	sysinfo["server_type"]="${virt_info:-未知}"
	sysinfo["glibc_version"]="${glibc_ver:-未知}"
	sysinfo["kernel_version"]="$kernel_ver"
	sysinfo["kernel_arch"]="$kernel_arch"
	sysinfo["cpu_info"]=$(escape_string "$(_print_cpu_info)")
	# 格式化输出到屏幕
	declare -A sections=(
		["服务器时间"]="${sysinfo[current_time]}"
		["操作系统版本"]="${sysinfo[os_version]}"
		["内核信息"]="$proc_ver"
		["服务器属性"]="${sysinfo[server_type]}"
		["CPU信息"]="$(_print_cpu_info)"
		["内存信息"]="$mem_info_m"
		["挂载信息"]="$fstab_info"
		["目录信息"]="$fs_info"
	)
	print_section() {
		local title=$1
		local content=$2
		color_printf blue "${title}:"
		printf "\n%s\n\n" "$content"
	}
	for title in "服务器时间" "操作系统版本" "内核信息" "服务器属性" "CPU信息" "内存信息" "挂载信息" "目录信息"; do
		print_section "$title" "${sections[$title]}"
	done
}
#========================================================
# 函数：conf_yum
# 作用：配置本地 YUM 源
#========================================================
function conf_yum() {
	# 截取已挂载的 ISO 镜像路径
	local mountPatch=
	local mount_output
	mount_output=$(run_cmd mount)
	while IFS= read -r line; do
		# 过滤出 iso9660 格式且非 dm8/dm9 及 /run/media 的挂载行
		if [[ "$line" =~ iso9660 ]] && ! [[ "$line" =~ dm8|dm9|/run/media ]]; then
			mountPatch=$(echo "$line" | awk '{print $(NF-3)}' | tail -n 1)
		fi
	done <<<"$mount_output"
	# 检查 ISO 是否已挂载
	if [[ -z $mountPatch ]]; then
		printf "\n"
		color_printf red "操作系统 ISO 镜像未挂载，且 -iso 参数配置为 Y，请先挂载操作系统镜像！"
		printf "\n"
		exit 1
	fi
	# 打印 yum 配置文件内容
	log_print "打印 yum 配置文件内容"
	local repo_path=/etc/yum.repos.d
	local repo_name="${repo_path}/local.repo"
	local config
	# 关闭订阅管理器插件，避免注册提示
	[[ -e /etc/yum/pluginconf.d/subscription-manager.conf ]] &&
		run_cmd sed -i 's/enabled=1/enabled=0/' /etc/yum/pluginconf.d/subscription-manager.conf
	# 关闭 debuginfo-install 插件，避免 Kylin/openEuler 注册提示
	[[ -e /etc/yum/pluginconf.d/debuginfo-install.conf ]] &&
		run_cmd sed -i 's/enabled=1/enabled=0/' /etc/yum/pluginconf.d/debuginfo-install.conf
	# 备份原有 yum 配置文件
	run_cmd mkdir -p ${repo_path}/bak && run_cmd mv -f ${repo_path}/* ${repo_path}/bak >/dev/null 2>&1
	# 定义 BaseOS 仓库
	_baseos() {
		config=$(
			cat <<EOF
[BaseOS]
name=BaseOS
baseurl=$baseurl_file
enabled=1
gpgcheck=0
EOF
		)
		run_cmd printf "%s\n" "$config" | run_cmd tee "$repo_name"
	}
	# 定义 AppStream 仓库
	_appstream() {
		config=$(
			cat <<EOF
[AppStream]
name=AppStream
baseurl=file://$mountPatch/AppStream
enabled=1
gpgcheck=0
EOF
		)
		run_cmd printf "%s\n" "$config" | run_cmd tee -a "$repo_name"
	}
	# 根据系统类型生成对应 repo 配置
	case $os_flag in
	OS1)
		local baseurl_file="file://$mountPatch"
		_baseos
		;;
	OS2)
		local baseurl_file="file://$mountPatch/BaseOS"
		_baseos
		_appstream
		;;
	OS3)
		local baseurl_file="file://$mountPatch"
		_baseos
		_appstream
		;;
	OS4)
		config=$(
			cat <<EOF
[openEuler]
name=openEuler
baseurl=file://$mountPatch
enabled=1
gpgcheck=1
gpgkey=file://$mountPatch/RPM-GPG-KEY-openEuler
EOF
		)
		run_cmd printf "%s\n" "$config" | run_cmd tee -a "$repo_name"
		;;
	esac
	# 清理旧缓存
	if ! run_cmd yum clean all >/dev/null 2>&1; then
		color_printf red "清理 yum 缓存失败，请检查系统日志！"
		exit 1
	fi
	printf "\n"
	# 重新生成缓存
	if ! run_cmd yum makecache >/dev/null 2>&1; then
		color_printf red "当前镜像仓库配置错误，请自行检查软件源配置！"
		exit 1
	else
		color_printf green "镜像仓库配置成功！"
	fi
}
#========================================================
# 函数：conf_port_firewall
# 作用：根据安装模式开放端口并禁用防火墙
#========================================================
function conf_port_firewall() {
	# 记录当前操作
	log_print "添加端口并禁用防火墙"
	# 按安装模式生成需开放的端口列表
	case $dm_install_mode in
	single)
		local -a ports=("$port_num")
		;;
	datawatch)
		local -a ports=("$port_num" "$mal_port_num" "$mal_dw_port_num" "$mal_inst_dw_port_num")
		;;
	raft)
		local -a ports=("$port_num" "$xmal_port")
		;;
	dsc | dscm)
		local -a ports=("$port_num" "$css_dcr_ep_port" "$asm_dcr_ep_port" "$db_dcr_ep_port" "$asvrmal_mal_port" "$db_mal_port")
		;;
	esac
	# 将端口列表写入全局信息数组
	sysinfo["firewall_port"]="$(
		IFS=' '
		echo "${ports[*]}"
	)"
	# RHEL/CentOS 6 使用 iptables
	if [[ $os_version -eq 6 ]]; then
		if run_cmd service iptables status >/dev/null 2>&1; then
			for port in "${ports[@]}"; do
				run_cmd iptables -A INPUT -s 0.0.0.0/0 -p tcp --dport "$port" -j ACCEPT
				[[ $? -eq 0 ]] && printf "端口 %s 已成功添加到 iptables 规则。\n" "$port" ||
					printf "添加端口 %s 到 iptables 规则失败。\n" "$port"
			done
			printf "\n"
			run_cmd iptables-save >/etc/sysconfig/iptables
			run_cmd service iptables stop
		else
			printf "防火墙服务未启动，无需禁用！\n"
			return
		fi
		run_cmd chkconfig iptables off
		run_cmd chkconfig ip6tables off
		run_cmd service iptables status
	# RHEL/CentOS 7+ 使用 firewalld
	else
		if [[ $(systemctl is-active firewalld) == "active" ]]; then
			for port in "${ports[@]}"; do
				run_cmd firewall-cmd -q --zone=public --add-port="$port/tcp" --permanent
				[[ $? -eq 0 ]] && printf "端口 %s 已成功添加到防火墙。\n" "$port" ||
					printf "添加端口 %s 到防火墙失败。\n" "$port"
			done
			run_cmd firewall-cmd -q --reload
			run_cmd systemctl stop firewalld.service -q
		else
			echo "防火墙服务未启动，无需禁用！"
			return
		fi
		printf "\n"
		run_cmd systemctl disable firewalld.service -q
		run_cmd systemctl status firewalld -q
	fi
}
#========================================================
# 函数：conf_selinux
# 作用：禁用 SELinux 并备份配置文件
#========================================================
function conf_selinux() {
	# 记录当前操作并备份配置文件
	log_print "禁用 SELinux"
	local config_file="/etc/selinux/config"
	bak_file "$config_file"
	# 根据当前 SELinux 状态执行禁用
	local status
	status=$(getenforce)
	case "$status" in
	Disabled)
		printf "SELinux 已经被禁用\n"
		;;
	Enforcing)
		printf "SELinux 正在强制执行，现在将其设置为禁用\n"
		run_cmd setenforce 0
		;;
	*)
		printf "SELinux 是宽容模式\n"
		;;
	esac
	run_cmd sed -i 's/SELINUX=.*/SELINUX=disabled/' "$config_file"
	if [[ $status != Disabled ]]; then
		printf "SELinux 已被设置为禁用，请重启系统以使更改生效\n"
	fi
	# 将最终 SELinux 配置写入全局信息数组
	sysinfo["selinux"]="$(grep '^SELINUX=' ${config_file})"
}
#========================================================
# 函数：conf_swap
# 作用：关闭 SWAP 功能
#========================================================
function conf_swap() {
	# 记录当前操作
	log_print "调整 SWAP 分区"
	local fstab_file="/etc/fstab"
	# 获取当前 SWAP 大小
	local -r swap=$(free | grep Swap | awk '{print $2}')
	# 若不存在 SWAP 分区则直接返回
	if [[ -z $swap ]]; then
		color_printf yellow "系统中不存在 SWAP 分区，无需调整 SWAP 分区！"
	else
		# 物理内存 > 40 GB 时关闭 SWAP
		if [[ $os_memory -gt 41943040 ]]; then
			run_cmd swapoff -a
			bak_file "$fstab_file"
			run_cmd sed -ri 's/.*swap.*/#&/' "$fstab_file"
		fi
		# 输出内存使用情况及 fstab 中 swap 行
		free -m
		printf "\n"
		grep <"$fstab_file" -v "^$" | grep "swap"
	fi
}
#========================================================
# 函数：_get_scheduler
# 说明：自动识别底层磁盘类型（SSD/HDD），
#      并返回推荐的 I/O 调度器名称。内部已整合调度器选择逻辑。
#========================================================
function _get_scheduler() {
	local dev rotational available candidates candidate flag
	declare -A rot_schedulers # 关联数组，键是rotational值，值是对应的调度算法
	# 获取所有磁盘设备
	local disks
	mapfile -t disks < <(lsblk -dno NAME 2>/dev/null | grep -vE '^(loop|sr0)')
	if [[ ${#disks[@]} -eq 0 ]]; then
		echo "[错误] 未找到任何磁盘设备" >&2
		return 1
	fi
	# 检查操作系统版本
	[[ ${os_version} -ne 6 ]] && flag=1
	# 遍历所有磁盘
	for dev in "${disks[@]}"; do
		# 读取磁盘属性
		local rot_file="/sys/block/$dev/queue/rotational"
		local sch_file="/sys/block/$dev/queue/scheduler"
		[[ -f "$rot_file" && -f "$sch_file" ]] || {
			echo "[警告] 设备 $dev 缺少必要的sysfs文件，跳过" >&2
			continue
		}
		rotational=$(cat "$rot_file")
		available=$(cat "$sch_file")
		# 完全保留原有的调度算法选择逻辑
		case "$rotational" in
		0) candidates=(${flag:+none} noop) ;;            # SSD
		1) candidates=(${flag:+mq-deadline} deadline) ;; # HDD
		*)
			continue
			;;
		esac
		# 为当前磁盘选择调度器
		for candidate in "${candidates[@]}"; do
			[[ -n "$candidate" && "$available" =~ $candidate ]] && {
				# 将找到的调度算法存入关联数组，键是rotational值
				rot_schedulers[$rotational]=$candidate
				break
			}
		done
	done
	# 优先检查是否有SSD（rot=0）
	echo "${rot_schedulers[0]:-${rot_schedulers[1]}}"
}
#========================================================
# 函数：conf_grub
# 作用：禁用透明大页、禁用 NUMA、开启 I/O 调度
#========================================================
function conf_grub() {
	log_print "禁用透明大页 & 禁用 NUMA & 开启 I/O 调度"
	local transparent_hugepage="transparent_hugepage=never"
	local numa="numa=off"
	# 动态获取推荐调度器
	local elevator
	elevator=$(_get_scheduler)
	[[ -n "$elevator" ]] && elevator="elevator=$elevator" || elevator=""
	# 更新参数的子函数
	_update_parameter() {
		local parameter=$1
		[[ -n "$parameter" ]] || return
		if ! run_cmd grubby --info=ALL | grep -q "$parameter"; then
			run_cmd grubby --update-kernel=ALL --args="$parameter"
		fi
	}
	# 组装并写入
	local options=("${transparent_hugepage}")
	[[ -n "$elevator" ]] && options+=("$elevator")
	options+=("${numa}")
	for option in "${options[@]}"; do
		_update_parameter "$option"
	done
	# 输出更新后的启动参数信息
	temp_file=$(mktemp)
	# 运行 grubby 并将输出保存到临时文件
	run_cmd grubby --info=ALL | awk '/numa/{print $0 "\n-" $(NR-1) "\n-" $(NR-2)}' | tee "$temp_file"
	# 添加信息到关联数组
	sysinfo["schedule"]="$(escape_string "$(cat "$temp_file")")"
}
#========================================================
# 函数：conf_sysctl
# 作用：配置内核参数和资源
#========================================================
function conf_sysctl() {
	log_print "配置内核参数和资源"
	local sysctl_file="/etc/sysctl.conf" name
	# 计算共享内存相关参数
	pagesize=$(getconf PAGE_SIZE) || true
	shmall=$(((os_memory - 10) * 1024 / pagesize)) || true
	shmmax=$((os_memory * 1024 - 10)) || true
	((shmall < 2097152)) && shmall=2097152
	((shmmax < 4294967295)) && shmmax=4294967295
	# 确定数据库实例名
	[[ "$dm_install_mode" =~ ^(single|datawatch|raft)$ ]] && name="$db_name" || name="$dsc_db_name"
	# 备份并追加内核参数
	bak_file "$sysctl_file"
	params=(
		"# DMBegin"
		"fs.aio-max-nr = 1048576"                                                      # 设置最大可用的异步 I/O 请求数
		"fs.file-max = 6815744"                                                        # 设置系统可以打开的最大文件数量
		"fs.nr_open = 20480000"                                                        # 设置每个进程可以打开的最大文件描述符数
		"kernel.core_pattern = ${core_dir}/${name}/core.%e.%p.%t"                      # 设置 core dump 文件的路径和命名规则
		"kernel.panic_on_oops = 1"                                                     # 当系统发生 Oops 错误时，是否触发 panic (1 表示触发)
		"kernel.numa_balancing = 0"                                                    # 禁用 NUMA（Non-Uniform Memory Access）平衡
		"kernel.randomize_va_space = 2"                                                # 启用地址空间布局随机化 (ASLR)，增加安全性
		"kernel.shmall = $shmall"                                                      # 设置共享内存段的最大值
		"kernel.shmmax = $shmmax"                                                      # 设置单个共享内存段的最大值
		"kernel.shmmni = 4096"                                                         # 设置系统可以使用的共享内存区块的最大数量
		"kernel.sem = 250 32000 100 128"                                               # 设置信号量集的大小限制
		"net.ipv4.ip_local_port_range = 9000 65500"                                    # 设置本地端口范围
		"net.ipv4.tcp_retries2 = 3"                                                    # 设置 TCP 连接重试次数
		"net.ipv4.tcp_fin_timeout = 5"                                                 # 设置 TCP FIN 等待超时的时间（单位：秒）
		"net.ipv4.tcp_synack_retries = 2"                                              # 设置 TCP SYN+ACK 重试次数
		"net.ipv4.tcp_syncookies = 1"                                                  # 启用 TCP 同步 cookie，以增加对 SYN flood 攻击的防御
		"net.ipv4.tcp_timestamps = 1"                                                  # 启用 TCP 时间戳，帮助诊断延迟问题
		"net.ipv4.tcp_rmem = 8192 87380 16777216"                                      # 设置 TCP 接收缓冲区的大小范围
		"net.ipv4.tcp_wmem = 8192 65536 16777216"                                      # 设置 TCP 发送缓冲区的大小范围
		"net.core.rmem_default = 262144"                                               # 设置默认的接收缓冲区大小
		"net.core.rmem_max = 4194304"                                                  # 设置最大接收缓冲区大小
		"net.core.wmem_default = 262144"                                               # 设置默认的发送缓冲区大小
		"net.core.wmem_max = 1048576"                                                  # 设置最大发送缓冲区大小
		"vm.swappiness = 10"                                                           # 设置系统交换空间的优先级，10 表示较少使用交换空间
		"vm.min_free_kbytes = $(free -k | awk 'NR==2 {printf "%d\n", int($2 / 200)}')" # 根据当前系统的内存使用情况，动态设置系统保留的最小内存
		"vm.numa_stat = 0"                                                             # 禁用 NUMA 统计功能
		"vm.overcommit_memory = 0"                                                     # 设置内存过度提交的行为，0 表示启用“谨慎”策略
		"vm.zone_reclaim_mode = 0"                                                     # 禁用内存区回收模式
		"# DMEnd"
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$sysctl_file" >/dev/null
	# 应用并保存生效的内核参数
	temp_file=$(mktemp)
	run_cmd sysctl -p | tee "$temp_file"
	sysinfo["kernel_params"]="$(escape_string "$(cat "$temp_file")")"
}
#========================================================
# 函数：conf_limits
# 作用：配置系统资源限制
#========================================================
function conf_limits() {
	log_print "配置系统资源"
	local limits_file="/etc/security/limits.conf"
	# 备份并追加 limits 配置
	bak_file "$limits_file"
	params=(
		"# DMBegin"
		"*     hard core 0"          # 设置硬限制：核心文件大小为0，表示不允许生成核心转储文件
		"*     soft core 0"          # 设置软限制：核心文件大小为0，表示不允许生成核心转储文件
		"dmdba soft core unlimited"  # 设置dmdba用户的软限制：核心文件大小无限制
		"dmdba hard core unlimited"  # 设置dmdba用户的硬限制：核心文件大小无限制
		"dmdba soft nproc 65536"     # 设置dmdba用户的软限制：最多可创建65536个进程
		"dmdba hard nproc 65536"     # 设置dmdba用户的硬限制：最多可创建65536个进程
		"dmdba soft nofile 65536"    # 设置dmdba用户的软限制：最多可打开65536个文件
		"dmdba hard nofile 65536"    # 设置dmdba用户的硬限制：最多可打开65536个文件
		"dmdba hard data unlimited"  # 设置dmdba用户的硬限制：数据段大小无限制
		"dmdba soft data unlimited"  # 设置dmdba用户的软限制：数据段大小无限制
		"dmdba hard fsize unlimited" # 设置dmdba用户的硬限制：最大文件大小无限制
		"dmdba soft fsize unlimited" # 设置dmdba用户的软限制：最大文件大小无限制
		"dmdba soft stack 65536"     # 设置dmdba用户的软限制：堆栈大小为65536KB
		"dmdba hard stack 65536"     # 设置dmdba用户的硬限制：堆栈大小为65536KB
		"# DMEnd"
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$limits_file" >/dev/null
	# 保存生效的 limits 配置
	temp_file=$(mktemp)
	run_cmd sed -n '/# DMBegin/,/# DMEnd/p' "$limits_file" | grep -v '^#|^*#|^$' | tee "$temp_file"
	sysinfo["user_resources"]="$(escape_string "$(cat "$temp_file")")"
	printf "\n"
	# 配置 PAM 以加载 limits.conf
	local login_file="/etc/pam.d/login"
	bak_file "$login_file"
	params=(
		"# DMBegin"
		"session required pam_limits.so"
		"session required /lib64/security/pam_limits.so"
		"# DMEnd"
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$login_file" >/dev/null
	# 保存生效的 PAM 配置
	temp_file=$(mktemp)
	run_cmd sed -n '/# DMBegin/,/# DMEnd/p' "$login_file" | grep -v '^#|^*#|^$' | tee "$temp_file"
	sysinfo["login_resources"]="$(escape_string "$(cat "$temp_file")")"
}
#========================================================
# 函数：conf_system_nproc
# 作用：配置 systemd 下的文件数与进程数限制
#========================================================
function conf_system_nproc() {
	log_print "配置文件数和进程数限制"
	local systemd_file="/etc/systemd/system.conf"
	# 仅对 CentOS/RHEL 7+ 生效
	if [[ $os_version -ne 6 ]]; then
		bak_file "$systemd_file"
		params=(
			"# DMBegin"
			"DefaultLimitFSIZE=unlimited" # 设置默认文件大小限制为无限制
			"DefaultLimitDATA=unlimited"  # 设置默认数据段大小限制为无限制
			"DefaultLimitCORE=unlimited"  # 设置默认核心文件大小限制为无限制
			"DefaultLimitNOFILE=65536"    # 设置默认打开文件数量限制为65536
			"DefaultLimitAS=unlimited"    # 设置默认地址空间大小限制为无限制
			"DefaultLimitNPROC=10240"     # 设置默认进程数量限制为10240
			"DefaultLimitNICE=0"          # 设置默认优先级调整限制为0
			"# DMEnd"
		)
		run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$systemd_file" >/dev/null
		# 保存生效的 systemd 配置
		temp_file=$(mktemp)
		run_cmd sed -n '/# DMBegin/,/# DMEnd/p' "$systemd_file" | grep -v '^#' | tee "$temp_file"
		if [[ $os_version -eq 6 ]]; then
			sysinfo["system"]="该系统不存在：${systemd_file}，无需配置"
		else
			sysinfo["system"]="$(escape_string "$(cat "$temp_file")")"
		fi
		printf "\n"
	fi
	# 创建 /etc/security/limits.d/dm_nproc.conf
	local nproc_file="/etc/security/limits.d/dm_nproc.conf"
	if check_file "$nproc_file"; then
		bak_file "$nproc_file"
		cat "$nproc_file"
	else
		params=(
			"# DMBegin"
			"dmdba soft nproc 65536" # 设置dmdba用户的软限制：最多可创建65536个进程
			"dmdba hard nproc 65536" # 设置dmdba用户的硬限制：最多可创建65536个进程
			"# DMEnd"
		)
		printf "%s\n" "${params[@]}" | tee "${nproc_file}" >"${nproc_file}.original"
		run_cmd sed -n '/# DMBegin/,/# DMEnd/p' "$nproc_file" | grep -v '^#|^*#|^$'
	fi
}
#========================================================
# 函数：conf_logind
# 作用：配置 systemd-logind 相关参数，避免会话中断
#========================================================
function conf_logind() {
	local logind_file="/etc/systemd/logind.conf"
	# CentOS/RHEL 6 无需配置
	[[ $os_version -eq 6 ]] && return 1
	log_print "配置与用户会话和系统登录管理相关的参数"
	# 检查文件是否存在
	if ! check_file "$logind_file"; then
		color_printf red "${logind_file}文件不存在，无法进行修改操作。"
		return 1
	fi
	# 避免注销用户时结束数据库进程
	current_value=$(loginctl show-session -p KillUserProcesses | cut -d= -f2 | tr -d '[:space:]')
	[[ "$current_value" == "yes" ]] && run_cmd sed -i '/^KillUserProcesses/Is/^/#/' "$logind_file"
	# 备份并追加 logind 配置
	bak_file "$logind_file"
	params=(
		"# DMBegin"
		"HandleSuspendKey=ignore"   # 忽略暂停键（不执行任何操作）
		"HandleHibernateKey=ignore" # 忽略休眠键（不执行任何操作）
		"IdleAction=ignore"         # 在空闲时不执行任何动作
		"KillUserProcesses=no"      # 不终止用户进程，即使用户注销时
		"RemoveIPC=no"              # 不在会话结束时移除IPC（Inter-Process Communication）资源
		"# DMEnd"
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$logind_file" >/dev/null
	# 重新加载并重启 systemd-logind
	run_cmd systemctl daemon-reload -q
	run_cmd systemctl restart systemd-logind -q
	# 显示最终配置
	run_cmd sed -n '/# DMBegin/,/# DMEnd/p' "$logind_file" | grep -v '^#|^*#|^$'
}
#========================================================
# 函数：conf_hostname
# 作用：根据安装模式设置主机名
#========================================================
function conf_hostname() {
	log_print "配置主机名"
	local network_file="/etc/sysconfig/network"
	local current_hostname host_name
	# 获取当前主机名
	current_hostname=$(hostname)
	# 计算目标主机名
	if [[ $dm_install_mode == "single" ]]; then
		host_name="$hostname"
	else
		host_name=$([[ $node_num -eq -1 ]] && echo "dwmon" || echo "${hostname}$(printf "%02d" "$node_num")")
	fi
	# 若目标主机名为空或与当前一致则直接返回
	if [[ -z "$hostname" || "$current_hostname" == "$host_name" ]]; then
		printf "当前主机名：%s，无需修改。\n" "$current_hostname"
		return 0
	fi
	# 按操作系统版本修改主机名
	if [[ $os_version -eq 6 ]]; then
		# CentOS/RHEL 6 使用传统方式
		bak_file "${network_file}"
		run_cmd /bin/hostname "$host_name"
		run_cmd sysctl kernel.hostname="$host_name"
		run_cmd printf "%s" "$host_name" | run_cmd tee /proc/sys/kernel/hostname >/dev/null
		run_cmd sed -i "s/HOSTNAME=$current_hostname/HOSTNAME=$host_name/" "${network_file}"
		printf "当前主机名：%s\n" "$(grep -E 'HOSTNAME=' "${network_file}" | cut -d= -f2)"
	else
		# CentOS/RHEL 7+ 使用 hostnamectl
		bak_file "/etc/hostname"
		run_cmd hostnamectl set-hostname "$host_name"
		printf "当前主机名：%s\n" "$(hostnamectl --static)"
	fi
}
#========================================================
# 函数：conf_hosts
# 作用：配置 /etc/hosts 文件，添加 DSC/DSCM 节点别名
#========================================================
function conf_hosts() {
	# 仅在 DSC/DSCM 且启用别名替换 IP 时执行
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ && $alias_replaces_ip =~ [Yy] ]]; then
		log_print "配置 hosts 文件"
		local hosts_file="/etc/hosts"
		local host_alias
		# 备份 hosts 并确保权限为 644
		bak_file "$hosts_file"
		[[ $(stat -c "%a" $hosts_file) != "644" ]] && chmod 644 $hosts_file
		# 写入注释起始标记
		run_cmd printf "%s\n" "# DMBegin" | run_cmd tee -a "$hosts_file" >/dev/null
		# 遍历所有 MAL IP，生成别名并写入 hosts
		for ((x = 0; x < ${#all_malip_arr[@]}; x++)); do
			ip="${all_malip_arr[$x]}"
			host_alias="$(upper "${dsc_ep_name}")$(printf "%02d" $((x + 1)))"
			run_cmd printf "%s %s\n" "$ip" "$host_alias" | run_cmd tee -a "$hosts_file" >/dev/null
			malip_host_arr+=("$host_alias")
		done
		# 显示新增内容
		grep -v "^[[:space:]]*\(#\|$\)" "$hosts_file"
	else
		# 未启用别名时直接使用 IP
		malip_host_arr=("${all_malip_arr[@]}")
	fi
}
#========================================================
# 函数：conf_ntp
# 作用：根据操作系统版本配置 NTP/Chrony 时间服务器
#========================================================
function conf_ntp() {
	local crontab_file="/var/spool/cron/root"
	local script_file="/usr/sbin/ntpdate"
	local ntp_file="/etc/ntp.conf"
	local ntpd_file="/etc/sysconfig/ntpd"
	local chrony_file="/etc/chrony.conf"
	local config
	case $os_version in
	6 | 7)
		# CentOS/RHEL 6/7 使用 ntpd
		bak_file "$ntp_file"
		bak_file /etc/sysconfig/ntpd
		# 清理旧的 server/pool 配置
		run_cmd sed -i -e '/^server/d' -e '/^pool/d' "$ntp_file"
		# 写入新的 ntp.conf
		config=$(
			cat <<EOF
tos maxdist 30
tinker panic 0
server $timeserver_ip iburst
server 127.127.1.0
fudge 127.127.1.0 stratum 8
notrust
EOF
		)
		run_cmd printf "%s\n" "$config" | run_cmd tee -a "$ntp_file" >/dev/null
		# 创建 ntpd PID 文件
		run_cmd touch /var/run/ntpd.pid
		# 写入 ntpd 启动参数
		config=$(
			cat <<EOF
OPTIONS="-g -x -p /var/run/ntpd.pid"
SYNC_HWCLOCK=yes
EOF
		)
		run_cmd printf "%s\n" "$config" | run_cmd tee -a "$ntpd_file" >/dev/null
		# 添加定时任务（若不存在）
		if ! crontab -l -u root | grep -q "$script_file"; then
			run_cmd printf "# DMBegin\n" | run_cmd tee -a "$crontab_file" >/dev/null
			run_cmd printf "10 5 * * * root %s -u %s >/dev/null 2>&1 ; hwclock -w\n" "$script_file" "$timeserver_ip" | run_cmd tee -a "$crontab_file" >/dev/null
		fi
		;;
	*)
		# CentOS/RHEL 8+ 使用 chronyd
		bak_file /etc/chrony.conf
		run_cmd sed -i -e '/^server/d' -e '/^pool/d' /etc/chrony.conf
		config=$(
			cat <<EOF
server $timeserver_ip iburst
allow $(ip route | grep "$(hostname -I)" | awk '{print $1}')
local stratum 10
EOF
		)
		run_cmd printf "%s\n" "$config" | run_cmd tee -a "$chrony_file" >/dev/null
		# 添加定时任务（若不存在）
		if ! run_cmd crontab -u dmdba -l | grep -q "$script_file"; then
			run_cmd printf "# DMBegin\n" | run_cmd tee -a "$crontab_file" >/dev/null
			run_cmd printf "10 5 * * * root %s -u %s >/dev/null 2>&1 ; hwclock -w\n" "$script_file" "$timeserver_ip" | run_cmd tee -a "$crontab_file" >/dev/null
		fi
		;;
	esac
}
#========================================================
# 函数：conf_timesync
# 作用：启用并启动 NTP/Chrony 服务，完成时间同步
#========================================================
function conf_timesync() {
	# 若未指定时间服务器则直接返回
	[[ -z $timeserver_ip ]] && return 1
	log_print "配置时间同步服务"
	# 按操作系统版本执行不同的时间同步流程
	case $os_version in
	6)
		# CentOS 6：使用 ntpd
		run_cmd service ntpd start
		run_cmd chkconfig ntpd on
		conf_ntp
		run_cmd /usr/sbin/ntpdate -u "$timeserver_ip" >/dev/null 2>&1
		run_cmd service ntpd restart
		run_cmd ntpstat
		printf "\n"
		run_cmd ntpq -p -n
		;;
	7)
		# CentOS 7：禁用 chronyd，启用 ntpd
		if systemctl is-active --quiet chronyd.service; then
			run_cmd systemctl stop chronyd.service
			run_cmd systemctl disable chronyd.service
			run_cmd mv /etc/chrony.conf /etc/chrony.conf.bak
		fi
		run_cmd systemctl start ntpd.service
		run_cmd systemctl enable ntpd.service
		conf_ntp
		run_cmd /usr/sbin/ntpdate -u "$timeserver_ip" >/dev/null 2>&1
		run_cmd systemctl restart ntpd.service
		run_cmd ntpstat
		printf "\n"
		run_cmd ntpq -p -n
		;;
	*)
		# CentOS 8+：禁用 ntpd，启用 chronyd
		if systemctl is-active --quiet ntpd.service; then
			run_cmd systemctl stop ntpd.service
			run_cmd systemctl disable ntpd.service
			run_cmd mv /etc/ntpd.conf /etc/ntpd.conf.bak
		fi
		run_cmd systemctl start chronyd.service
		run_cmd systemctl enable chronyd.service
		conf_ntp
		/usr/sbin/ntpdate -u "$timeserver_ip" >/dev/null 2>&1
		run_cmd systemctl restart chronyd.service
		run_cmd chronyc tracking
		printf "\n"
		run_cmd chronyc sources -v
		;;
	esac
}
#========================================================
# 函数：conf_drop_cache
# 作用：创建定时任务脚本，每日 06:00 清理页缓存
#========================================================
function conf_drop_cache() {
	log_print "配置 cache 脚本"
	local crontab_file="/var/spool/cron/root"
	local script_file="/root/drop_cache.sh"
	# 备份 crontab
	bak_file "$crontab_file"
	# 生成缓存清理脚本
	params=(
		"#!/usr/bin/env bash"
		"# DMBegin"
		"sync"
		"echo 1 > /proc/sys/vm/drop_caches"
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee "$script_file" >/dev/null
	run_cmd chmod 775 "$script_file"
	# 添加每日 06:00 定时任务（若不存在）
	if ! run_cmd crontab -l -u root | run_cmd grep -q "$script_file" >/dev/null 2>&1; then
		run_cmd printf "0 6 * * * root %s\n" "$script_file" | run_cmd tee -a "$crontab_file" >/dev/null
	fi
	# 展示脚本与当前 crontab
	run_cmd cat "$script_file" && run_cmd crontab -l -u root
}
#========================================================
# 函数：conf_profile
# 作用：配置系统级 /etc/profile 及 locale
#========================================================
function conf_profile() {
	log_print "配置/etc/profile"
	local locale_file="/etc/locale.conf"
	# 内部函数：统一备份并写入 locale 配置
	_update_locale() {
		local locale_file="$1"
		local lang_value="$2"
		bak_file "$locale_file"
		params=(
			"# DMBegin"
			"LANG=$lang_value"
			"LC_CTYPE=$lang_value"
			"# DMEnd"
		)
		run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$locale_file" >/dev/null
		grep <"${locale_file}" "zh_CN.utf8\|en_US.utf8"
	}
	# CentOS/RHEL 7+ 设置 locale
	if [[ $os_version -ne 6 ]]; then
		if run_cmd locale -a | grep -qw "zh_CN.utf8"; then
			_update_locale "$locale_file" "zh_CN.utf8"
		else
			_update_locale "$locale_file" "en_US.utf8"
		fi
	fi
	# 追加全局环境变量到 /etc/profile
	local profile_file="/etc/profile"
	bak_file "${profile_file}"
	if locale -a | grep -q "zh_CN.utf8"; then
		params=(
			"# DMBegin"
			"export LANG=zh_CN.UTF-8"
		)
	else
		params=(
			"# DMBegin"
			"export LANG=en_US.UTF-8"
		)
	fi
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$profile_file" >/dev/null
	awk '/export LANG/{print}' "$profile_file" | grep -v '^#\|^*#\|^$'
	printf "\n"
	# 限制 glibc 内存池数量，减少虚拟内存占用
	params=(
		"export MALLOC_ARENA_MAX=4"
		"# DMEnd"
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$profile_file" >/dev/null
	run_cmd bash -l -c "source /etc/profile"
	run_cmd awk '/export MALLOC_ARENA_MAX/{print}' "$profile_file" | grep -v '^#\|^*#\|^$'
	# 保存最终 profile 内容到全局信息
	sysinfo["profile"]="$(escape_string "$(sed -n '/# DMBegin/,/# DMEnd/p' "${profile_file}")")"
}
#========================================================
# 函数：create_os_user
# 作用：创建 dmdba 用户及相关组，并设置密码与 sudo
#========================================================
function create_os_user() {
	log_print "开始创建 dmdba 用户，密码为：$dm_passwd"
	# 变量定义
	local dinstall_group="dinstall"
	local dmdba_group="dmdba"
	local dmsso_group="dmsso"
	local dmauditor_group="dmauditor"
	local dmdba_id=56781
	local dmdba_user="dmdba"
	# 内部函数：创建组（自动处理 ID 冲突）
	_create_group() {
		local group=$1
		local group_id=$2
		while grep -q ":$group_id:" /etc/group; do
			group_id=$((group_id + 1234))
		done
		if ! grep -q "^$group:" /etc/group; then
			run_cmd /usr/sbin/groupadd -g "$group_id" "$group"
		fi
	}
	# 内部函数：创建或修改用户
	_create_user() {
		local user=$1
		local base_cmd=""
		while grep -q ":$dmdba_id:" /etc/passwd; do
			dmdba_id=$((dmdba_id + 1234))
		done
		if ! grep -q "^$user:" /etc/passwd; then
			base_cmd="/usr/sbin/useradd -u $dmdba_id -g $dinstall_group"
		else
			base_cmd="/usr/sbin/usermod -g $dinstall_group"
		fi
		[[ $os_auth =~ [Yy] ]] && base_cmd="$base_cmd -G $dmdba_group,$dmsso_group,$dmauditor_group"
		base_cmd="$base_cmd -s /bin/bash -d /home/$user -m $user"
		eval "$base_cmd" >/dev/null 2>&1
	}
	# 创建所需组
	_create_group $dinstall_group 56781
	[[ $os_auth =~ [Yy] ]] && {
		_create_group $dmdba_group 56782
		_create_group $dmsso_group 56783
		_create_group $dmauditor_group 56784
	}
	# 创建/更新用户并设置密码
	_create_user $dmdba_user
	echo "$dmdba_user:$dm_passwd" | run_cmd /usr/sbin/chpasswd 2>/dev/null
	grep </etc/passwd $dmdba_user
	printf "\n"
	id $dmdba_user
	# 若仅创建用户，则配置 sudo 免密
	if [[ $only_create_user =~ [Yy] ]]; then
		local sudoers_file=/etc/sudoers.d/dmdba
		params=(
			"# DMBegin"
			"dmdba ALL=(ALL) NOPASSWD: ALL"
		)
		run_cmd printf "%s\n" "${params[@]}" | run_cmd tee "$sudoers_file" >/dev/null
		chmod 0440 "$sudoers_file"
		printf "\n"
		printf "%s用户密码是：%s\n" "$dmdba_user" "$(color_printf green "$dm_passwd")"
		printf "\n"
	fi
}
#========================================================
# 函数：conf_user_profile
# 作用：配置 root 与 dmdba 用户的个性化环境变量及别名
#========================================================
function conf_user_profile() {
	log_print "配置用户环境变量"
	color_printf blue "配置 root 环境变量"
	printf "\n"
	# 配置 root 用户 bash_profile
	local bash_profile_file="/root/.bash_profile"
	bak_file "$bash_profile_file"
	params=(
		"# DMBegin"
		"alias sd='su - dmdba'"
		"export PS1=\"[\$(whoami)@\$(hostname):\"'\$PWD]# '"
		"# DMEnd"
	)
	[[ $(run_cmd grep -E -c '# DMBegin' "$bash_profile_file") -eq 0 ]] && run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$bash_profile_file" >/dev/null
	# 保存 root 环境变量到全局信息
	temp_file=$(mktemp)
	run_cmd grep -E -v '^#|^$' "$bash_profile_file" | tee "$temp_file"
	sysinfo["root_env"]="$(escape_string "$(cat "${temp_file}")")"
	printf "\n"
	# 配置 dmdba 用户 bash_profile
	color_printf blue "配置 dmdba 环境变量"
	printf "\n"
	# 计算数据库实例路径
	local data_path
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
		data_path="$data_dir"/"$dsc_db_name"
	else
		data_path="$([[ $node_num -eq -1 ]] && echo "$env_dm_home/data/$db_name" || echo "$data_dir/$db_name")"
	fi
	bash_profile_file="/home/dmdba/.bash_profile"
	bak_file "$bash_profile_file"
	params=(
		"# DMBegin"
		"export LD_LIBRARY_PATH=\"/usr/lib64:\$LD_LIBRARY_PATH:$env_dm_home/bin:$env_dm_home/bin/dependencies\""
		"export DM_HOME=\"$env_dm_home\""
		"export JAVA_HOME=\"\${DM_HOME}/jdk\""
		"export JRE_HOME=\"\${JAVA_HOME}/jre\""
		"export CLASSPATH=\"\${JAVA_HOME}/lib:\${JRE_HOME}/lib:\$CLASSPATH\""
		"export PATH=\"\$PATH:\$JAVA_HOME/bin:\$JRE_HOME/bin:\$DM_HOME/bin:\$DM_HOME/tool\""
		"export PS1=\"[\$(whoami)@\$(hostname):\"'\$PWD]$ '"
		"alias dmbin=\"cd \${DM_HOME}/bin\""
		"alias dmlog=\"cd \${DM_HOME}/log\""
		"alias dmdata=\"cd ${data_path}\""
	)
	run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$bash_profile_file" >/dev/null
	# 根据安装模式追加专用别名
	case $dm_install_mode in
	"single" | "raft")
		run_cmd printf 'alias sqllog="cd %s/%s/sqllog"\n' "$backup_dir" "$db_name" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias slog="vi %s/log/dm_%s_%s.log"\n' "\${DM_HOME}" "$(upper "${instance_name}")" "\$(date +%Y%m)" | run_cmd tee -a "$bash_profile_file" >/dev/null
		;;
	"datawatch")
		run_cmd printf 'alias sqllog="cd %s/%s/sqllog"\n' "$backup_dir" "$db_name" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias dm="dmmonitor %s/dmmonitor_%s.ini"\n' "$data_path" "$dw_group_name" | run_cmd tee -a "$bash_profile_file" >/dev/null
		if [[ $node_num -ne -1 ]]; then
			run_cmd printf 'alias wlog="vi %s/log/dm_dmwatcher_%s_%s.log"\n' "\${DM_HOME}" "$(upper "${instance_name}")" "\$(date +%Y%m)" | run_cmd tee -a "$bash_profile_file" >/dev/null
			run_cmd printf 'alias slog="vi %s/log/dm_%s_%s.log"\n' "\${DM_HOME}" "$(upper "${instance_name}")" "\$(date +%Y%m)" | run_cmd tee -a "$bash_profile_file" >/dev/null
		fi
		;;
	"dsc" | "dscm")
		tool=$([ "$dm_install_mode" = "dsc" ] && echo "dmasmtool" || echo "dmasmtoolm")
		run_cmd printf 'alias sqllog="cd %s/%s/sqllog"\n' "$data_path" "$(lower "$dsc_ep_name")$(printf "%02d" "$node_num")_config" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias dc="dmcssm %s/dmcssm.ini"\n' "$data_path" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias dt="%s DCR_INI=%s/dmdcr.ini"\n' "$tool" "$data_path" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias clog="vi %s/log/dm_%s_%s.log"\n' "\${DM_HOME}" "CSS$(printf "%02d" $((node_num)))" "\$(date +%Y%m)" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias alog="vi %s/log/dm_%s_%s.log"\n' "\${DM_HOME}" "ASM$(printf "%02d" $((node_num)))" "\$(date +%Y%m)" | run_cmd tee -a "$bash_profile_file" >/dev/null
		run_cmd printf 'alias slog="vi %s/log/dm_%s_%s.log"\n' "\${DM_HOME}" "$(upper "${dsc_ep_name}")$(printf "%02d" $((node_num)))" "\$(date +%Y%m)" | run_cmd tee -a "$bash_profile_file" >/dev/null
		;;
	esac
	# 若启用 OS 认证，追加免密登录别名
	if [[ $os_auth =~ [Yy] ]]; then
		params=(
			"alias ds=\"disql -L /:$port_num as sysdba\""
		)
		run_cmd printf "%s\n" "${params[@]}" | run_cmd tee -a "$bash_profile_file" >/dev/null
	fi
	# 写入结束标记并保存 dmdba 环境变量
	run_cmd printf "%s\n" "# DMEnd" | run_cmd tee -a "$bash_profile_file" >/dev/null
	temp_file=$(mktemp)
	run_cmd grep -E -v '^#|^$' "$bash_profile_file" | tee "$temp_file"
	sysinfo["dmdba_env"]="$(escape_string "$(cat "${temp_file}")")"
}
#========================================================
# 函数：create_env_dir
# 作用：创建或复用达梦安装目录，并处理旧目录/进程
#========================================================
function create_env_dir() {
	local proc_pat
	# 根据安装模式确定进程匹配关键字
	case $dm_install_mode in
	datawatch) proc_pat="dmmonitor|${db_name}/dm.ini|${db_name}/dmwatcher.ini" ;;
	dsc | dscm) proc_pat="${dsc_db_name}/dmdcr.ini" ;;
	*) proc_pat="${db_name}/dm.ini" ;;
	esac
	# 内部函数：通过运行进程反推出 $env_dm_home
	_get_dm_home_by_pid() {
		local pat=$1
		local cmdline
		if [[ $os_version -eq 6 ]]; then
			cmdline=$(ps -ef | grep -E "dmap|dmimon|$pat" | grep -v grep | awk '{print $8}' | head -1)
			[[ -n $cmdline ]] && echo "${cmdline%%/bin*}"
		else
			cmdline=$(pgrep -af "dmap|dmimon|$pat" | awk '{print $2}' | head -1)
			[[ -n $cmdline ]] && echo "${cmdline%%/bin*}"
		fi
	}
	# 仅初始化实例：复用已有安装目录
	if [[ $only_init_db =~ [Yy] ]]; then
		if pgrep -f "dmap|dmimon|$proc_pat" >/dev/null 2>&1; then
			env_dm_home=$(_get_dm_home_by_pid "$proc_pat")
			[[ -z $env_dm_home ]] && {
				color_printf red "无法获取 env_dm_home 路径，进程状态异常"
				exit 1
			}
		fi
		return
	fi
	# 正常安装：杀进程 → 备份旧目录 → 创建新目录
	pgrep -f "dmap|dmimon|$proc_pat" >/dev/null 2>&1 && {
		pgrep -f "dmap|dmimon|$proc_pat" | xargs kill -9 2>/dev/null
		wait
	}
	[[ -d $env_dm_home && -n $(ls -A "$env_dm_home") ]] &&
		run_cmd mv "$env_dm_home" "${env_dm_home}${current}"
	[[ ! -d $env_dm_home ]] && {
		run_cmd mkdir -p -m 755 "$env_dm_home"
		run_cmd chown dmdba:dinstall "$env_dm_home"
	}
}
#========================================================
# 函数：mount_iso
# 作用：挂载达梦安装镜像到临时目录
#========================================================
function mount_iso() {
	log_print "挂载 DM ISO 文件"
	# 卸载历史挂载点（含 dm8/dm9）
	while IFS= read -r line; do
		if [[ $line =~ dm8|dm9 && $line =~ iso9660 ]]; then
			device=$(echo "$line" | awk '{print $(NF-3)}')
			run_cmd umount -f "$device"
		fi
	done <<<"$(run_cmd mount)"
	# 清理空目录
	run_cmd find "$software_dir" -type d -empty -delete 2>/dev/null
	# 创建随机挂载目录
	export mount_dir="${software_dir}/${random_string}"
	[[ ! -d $mount_dir ]] && run_cmd mkdir -p "$mount_dir"
	# 卸载系统默认挂载点
	[[ -d /run/media/root/dameng ]] && run_cmd umount -f /run/media/root/dameng
	# 执行挂载
	run_cmd mount -t auto -o ro,loop "${software_dir}/$dm_iso_name" "$mount_dir"
	ls -lrth "$mount_dir"
	# 仅配置操作系统
	[[ $only_conf_os =~ [Yy] ]] && printf "\n"
}
#========================================================
# 函数：install_dmdb
# 作用：安装达梦数据库软件（支持仅初始化场景）
#========================================================
function install_dmdb() {
	local dm_svc_file="/etc/dm_svc.conf"
	local xml_file="${software_dir}/auto_install.xml"
	local install_bin lang key_file
	# 仅初始化实例：若软件已装则直接返回
	if [[ $only_init_db =~ [Yy] ]]; then
		if run_cmd su - dmdba -c "$env_dm_home/bin/disql -id" &>/dev/null; then
			log_print "达梦数据库软件已安装"
			run_cmd su - dmdba -c "$env_dm_home/bin/disql -id"
			run_cmd su - dmdba -c "$env_dm_home/bin/dminit help" 2>/dev/null | grep -qP "SYSSSO_PWD(?!=[.])" && db_flag=true
			return
		fi
	fi
	log_print "开始安装达梦数据库软件"
	# 生成静默安装 XML
	lang=$([[ $LANG == "en_US.UTF-8" ]] && echo "en" || echo "zh")
	# 判断是否存在授权文件
	[[ -f "${software_dir}/dm${oguid}.key" ]] && key_file="${software_dir}/dm${oguid}.key" || key_file=""
	# 按照数据库版本生成xml文件
	case $db_version in
	"8")
		# 生成dm8的xml文件
		config=$(
			cat <<EOF
<?xml version="1.0"?>
<DATABASE>
   <LANGUAGE>$lang</LANGUAGE>
   <TIME_ZONE>+08:00</TIME_ZONE>
   <KEY>${key_file}</KEY>
   <INSTALL_TYPE>0</INSTALL_TYPE>
   <INSTALL_PATH>${env_dm_home}</INSTALL_PATH>
   <INIT_DB>N</INIT_DB>
</DATABASE>
EOF
		)
		;;
	"9")
		# 生成dm9的xml文件
		config=$(
			cat <<EOF
<?xml version="1.0"?> 
<DATABASE> 
   <LANGUAGE>$lang</LANGUAGE> 
   <KEY>${key_file}</KEY> 
   <SERVER>Y</SERVER> 
   <CLIENT>Y</CLIENT> 
   <DRIVER>Y</DRIVER> 
   <MANUAL>Y</MANUAL> 
   <INSTALL_PATH>${env_dm_home}</INSTALL_PATH> 
</DATABASE>
EOF
		)
		;;
	esac
	# 使用 printf 和 run_cmd tee 写入 XML 配置文件
	run_cmd printf "%s\n" "$config" | tee "${software_dir}/auto_install.xml" >/dev/null
	# 定位安装程序
	if [[ -n $dm_iso_name && -f $software_dir/$random_string/DMInstall.bin ]]; then
		install_bin="$software_dir/$random_string/DMInstall.bin"
	elif [[ -f $software_dir/DMInstall.bin ]]; then
		install_bin="$software_dir/DMInstall.bin"
	else
		color_printf red "找不到达梦数据库安装程序 DMInstall.bin"
		exit 1
	fi
	# 重装时，备份
	[[ -f ${dm_svc_file} ]] && run_cmd "$CP_CMD" -prf "$dm_svc_file"{,."${current}"}
	# 执行静默安装
	run_cmd chmod -f +x "$install_bin"
	run_cmd bash -l -c "export DM_INSTALL_TMPDIR='$software_dir'; ulimit -n 65536; '$install_bin' -q '$xml_file' 2>&1" || {
		color_printf red "达梦数据库软件安装失败"
		[[ -f ${dm_svc_file}.${current} ]] && run_cmd "$CP_CMD" -prf "${dm_svc_file}.${current}" "$dm_svc_file"
		exit 1
	}
	# 备份 dm_svc.conf
	bak_file "$dm_svc_file"
	# 安装后检查是否为安全版
	run_cmd su - dmdba -c "$env_dm_home/bin/dminit help" 2>/dev/null | grep -qP "SYSSSO_PWD(?!=[.])" && db_flag=true
	# CentOS6 DSC 环境补充软链接
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ && $os_version -eq 6 ]]; then
		local so_cnt
		so_cnt=$(find /lib64 -maxdepth 1 -name 'libdm*.so' | wc -l)
		((so_cnt <= 6)) && run_cmd ln -sf "$env_dm_home"/bin/*.so /lib64
	fi
}
#========================================================
# 函数：conf_glogin
# 作用：配置 disql 登录后自动执行的 glogin.sql，统一输出格式
#========================================================
function conf_glogin() {
	# 打印日志，标识函数开始
	log_print "配置 glogin.sql"
	local glogin_file="$env_dm_home/bin/disql_conf/glogin.sql"
	# 备份原 glogin.sql
	bak_file "$glogin_file"
	local config
	# 生成新的 glogin.sql 配置内容
	config=$(
		cat <<EOF
-- 1.配置环境变量
-- 设置dbms_output输出缓冲区大小
set serveroutput on size 1000000
-- 设置 blob、clob、char、varchar、binary、varbinary、class 等类型一列能显示的最大字节数
set long 200
-- 设置屏幕上一行显示宽度
set linesize 500
-- 设置一页有多少行数
set pagesize 5000
-- 对于 spool 文件，是否去除输出每行的结尾空格，缺省为 off
set trimspool on
-- 去除行号显示
set lineshow off
-- 显示系统的当前时间
set time on
-- 2. 显示密钥过期时间
col edate new_value _edate
select to_char(expired_date, 'yyyy-mm-dd') edate from v\$license;
host echo "密钥过期时间：&_edate"
-- 3. 动态提示符
set sqlprompt "_USER'@'_connect_identifier SQL> "
EOF
	)
	# 写入配置文件
	run_cmd printf "%s\n" "$config" | run_cmd tee "$glogin_file" >/dev/null
	# 打印非注释行确认配置
	grep -v "^[[:space:]]*\(#\|$\|--\)" "$glogin_file"
	# 格式化日志输出
	[[ $install_until_db =~ [Yy] ]] && printf "\n"
}
#========================================================
# 函数：create_ins_dir
# 作用：创建实例所需各类目录（数据、备份、归档、核心转储等）
#========================================================
function create_ins_dir() {
	local name str target_dir second_path
	# 统一进程匹配串
	case $dm_install_mode in
	datawatch) str="dmmonitor|${db_name}/dm.ini|${db_name}/dmwatcher.ini" ;;
	dsc | dscm) str="${dsc_db_name}/dmdcr.ini" ;;
	*) str="${db_name}/dm.ini" ;;
	esac
	# 仅初始化时杀进程
	[[ $only_init_db =~ [Yy] ]] &&
		pgrep -f "$str" &>/dev/null &&
		pgrep -f "$str" | xargs -r kill -9 && wait
	# 统一实例名
	name=$([[ $dm_install_mode =~ ^(single|datawatch|raft)$ ]] && echo "$db_name" || echo "$dsc_db_name")
	# 遍历目录数组，创建或备份实例子目录
	local dirs=("$data_dir" "$backup_dir" "$arch_dir" "$core_dir")
	for d in "${dirs[@]}"; do
		[[ -z $d ]] && continue
		target_dir="$d/$name"
		[[ -d $target_dir ]] && run_cmd mv "$target_dir" "${target_dir}${current}"
		run_cmd mkdir -p -m 755 "$target_dir"
		second_path=$(awk -F'/' 'NF>=3{print "/"$2}' <<<"$d")
		[[ -n $second_path && -d $second_path && $second_path != "/" ]] &&
			run_cmd chmod 755 "$second_path"
	done
	# 创建脚本目录
	[[ ! -d $scripts_dir ]] && run_cmd mkdir -p -m 755 "$scripts_dir"
	# 按模式创建专用目录
	case $dm_install_mode in
	datawatch)
		[[ $node_num -eq -1 && ! -d $env_dm_home/data/$db_name ]] && {
			run_cmd mkdir -p -m 755 "$env_dm_home/data/$db_name"
			run_cmd chown -R dmdba:dinstall "$env_dm_home/data"
		}
		;;
	dsc | dscm)
		[[ ! -d $data_dir/$dsc_db_name ]] &&
			run_cmd mkdir -p -m 755 "$data_dir/$dsc_db_name"
		;;
	esac
	# 统一赋权
	run_cmd chown -R dmdba:dinstall "${dirs[@]}" "$scripts_dir"
	# 收集磁盘信息
	_get_dir_info() {
		local p=$1
		local line
		line=$(df -h "$p" | awk 'NR==2{printf "总大小:%s 已用:%s 可用:%s 使用率:%s",$2,$3,$4,$5}')
		printf "%s\n%s" "$p" "$line"
	}
	sysinfo[install_dir]=$(escape_string "$(_get_dir_info "$env_dm_home")")
	sysinfo[instance_dir]=$(escape_string "$(_get_dir_info "$data_dir")")
	sysinfo[archive_dir]=$(escape_string "$(_get_dir_info "$arch_dir")")
	sysinfo[backup_dir]=$(escape_string "$(_get_dir_info "$backup_dir")")
}
#========================================================
# 函数：init_database
# 作用：使用 dminit 初始化数据库实例
#========================================================
function init_database() {
	# 打印日志，标识函数开始
	log_print "初始化达梦数据库"
	# 检查必要参数
	if [ -z "$data_dir" ] || [ -z "$db_name" ] || [ -z "$instance_name" ] || [ -z "$port_num" ]; then
		color_printf red "错误：必须提供 data_dir、db_name、instance_name 和 port_num 参数"
		exit 1
	fi
	# 构建 dminit 参数字符串
	local dm_init_cmd="$env_dm_home/bin/dminit"
	local dm_init_para="PATH=$data_dir \
AUTO_OVERWRITE=2 \
EXTENT_SIZE=$extent_size \
PAGE_SIZE=$page_size \
CASE_SENSITIVE=$case_sensitive \
CHARSET=$charset \
LOG_SIZE=$log_size \
DB_NAME=$db_name \
INSTANCE_NAME=$instance_name \
PORT_NUM=$port_num \
BLANK_PAD_MODE=$blank_pad_mode \
SYSDBA_PWD='${sysdba_pwd_temp}' \
SYSAUDITOR_PWD='${sysauditor_pwd_temp}'"
	# 安全版追加 SYSSSO 密码
	[[ "$db_flag" == "true" ]] && dm_init_para="$dm_init_para SYSSSO_PWD='${syssso_pwd_temp}'"
	# 追加 LENGTH_IN_CHAR 参数
	[[ $length_in_char =~ [Yy1] ]] && dm_init_para="$dm_init_para LENGTH_IN_CHAR=$length_in_char"
	# 如果存在RLOG_POSTFIX_NAME参数，则追加此参数
	if run_cmd su - dmdba -c "$env_dm_home/bin/dminit help" 2>/dev/null | grep -q "RLOG_POSTFIX_NAME"; then
		dm_init_para="$dm_init_para RLOG_POSTFIX_NAME=${rlog_postfix_name}"
	fi
	# 追加自定义参数
	if [[ -n "$dminit_para_value" ]]; then
		dminit_para_value=$(echo "$dminit_para_value" | awk '{gsub(/ *= */, "="); print}')
		dm_init_para="$dm_init_para $dminit_para_value"
	fi
	# 执行 dminit 命令
	run_cmd su - dmdba -c "$dm_init_cmd $dm_init_para"
	# 检查执行结果
	[ $? -ne 0 ] && {
		color_printf red "错误：执行 dminit 命令失败"
		exit 1
	}
	# 获取版本信息
	local db_build_version
	db_build_version="$(run_cmd su - dmdba -c "$env_dm_home/bin/dminit help" 2>/dev/null | grep -i version | tail -n 1 | awk -F':' '{print $2}')"
	# 记录到全局信息
	sysinfo["db_ver"]="DM${db_version}"
	sysinfo["db_build_version"]="${db_build_version}"
	sysinfo["db_case_sensitive"]="$([[ "$case_sensitive" =~ [Y1] ]] && echo "敏感" || echo "不敏感")"
	sysinfo["db_charset"]="$([[ "$charset" -eq 0 ]] && echo "GB18030" || echo "UTF8")"
	sysinfo["db_char_unit"]="$([[ "$length_in_char" =~ [Yy1] ]] && echo "以字符为单位" || echo "以字节为单位")"
	sysinfo["db_log_count"]="2"
}
#========================================================
# 函数：conf_sqllog
# 作用：为每个节点生成 sqllog.ini，开启 SQL 异步追踪
#========================================================
function conf_sqllog() {
	local db_dir
	# 内部函数，生成 sqllog.ini 内容
	_create_ini_file() {
		local db_dir=$1
		local config
		config=$(
			cat <<EOF
BUF_TOTAL_SIZE          = 204800
BUF_SIZE                = 10240
BUF_KEEP_CNT            = 20
[SLOG_ALL]
FILE_PATH    = $db_dir
PART_STOR    = 0
SWITCH_MODE  = 2
SWITCH_LIMIT = 128
ASYNC_FLUSH  = 1
FILE_NUM     = 200
ITEMS        = 0
SQL_TRACE_MASK  = 1
MIN_EXEC_TIME = 0
USER_MODE   = 0
USERS       =
EXECTIME_PREC_FLAG = 0
[SLOG_ERROR]
SQL_TRACE_MASK = 23
FILE_PATH      = $db_dir
[SLOG_DDL]
SQL_TRACE_MASK = 3
[SLOG_LONG_SQL]
SQL_TRACE_MASK = 25
MIN_EXEC_TIME = 60000
EOF
		)
		run_cmd su - dmdba <<EOF
printf "%s" "$config" | tee "${db_dir}.ini" > /dev/null
EOF
	}
	# 根据安装模式创建 sqllog 目录与配置
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
		for ((x = 0; x < ${#all_pubip_arr[@]}; x++)); do
			ep_name="${dsc_ep_name}$(printf "%02d" $((x + 1)))"
			db_dir="$data_dir/$dsc_db_name/$(lower "$ep_name")_config/sqllog"
			run_cmd mkdir -p "$db_dir"
			run_cmd chown -R dmdba:dinstall "$db_dir"
			_create_ini_file "$db_dir"
		done
	else
		db_dir="$data_dir/$db_name/sqllog"
		_create_ini_file "$db_dir"
		run_cmd sed -i "s#$data_dir#$backup_dir#g" "${db_dir}.ini"
		run_cmd mkdir -p "$backup_dir/$db_name/sqllog"
		run_cmd chown -R dmdba:dinstall "$backup_dir/$db_name/sqllog"
		sysinfo["sqllogIni"]="$(escape_string "$(cat "${db_dir}.ini")")"
	fi
}
#========================================================
# 函数：start_database
# 作用：注册并启动数据库服务，收集授权信息
#========================================================
function start_database() {
	# 打印日志，标识函数开始
	log_print "注册数据库服务"
	readonly SERVICE_TYPE="dmserver"
	readonly CONFIG_FILE="$data_dir/$db_name/dm.ini"
	local WAIT_TIME="1s"
	# 注册新服务
	run_cmd "$env_dm_home"/script/root/dm_service_installer.sh -t "$SERVICE_TYPE" -dm_ini "$CONFIG_FILE" -p "$db_name"
	printf "\n"
	# 前台静默启停一次，确保系统表初始化
	readonly DM_SERVER_CMD="$env_dm_home/bin/dmserver"
	run_cmd su - dmdba -c "\"$DM_SERVER_CMD $data_dir/$db_name/dm.ini\" <<-EOF
exit
EOF
" >/dev/null 2>&1
	# 启动数据库
	run_cmd su - dmdba -c "$DMS_START_CMD"
	# 等待数据库 OPEN
	check_db_status "OPEN"
	# 收集授权信息
	IFS='|' read -r lic_version series_no server_series server_type project_name \
		expired_date authorized_customer authorized_user_number \
		concurrency_user_number max_cpu_num max_core_num cluster_type db_create_time db_startup_time <<<"$(
			execute_sql "SYSDBA" "$sysdba_pwd_temp" "SELECT 
L.LIC_VERSION || '|' || 
L.SERIES_NO || '|' || 
L.SERVER_SERIES || '|' || 
L.SERVER_TYPE || '|' || 
NVL(L.PROJECT_NAME, '') || '|' || 
TO_CHAR(L.EXPIRED_DATE, 'YYYY-MM-DD') || '|' || 
L.AUTHORIZED_CUSTOMER || '|' || 
L.AUTHORIZED_USER_NUMBER || '|' || 
L.CONCURRENCY_USER_NUMBER || '|' || 
L.MAX_CPU_NUM || '|' || 
L.MAX_CORE_NUM || '|' || 
L.CLUSTER_TYPE || '|' ||
D.CREATE_TIME || '|' || 
I.START_TIME 
FROM 
V\\\$LICENSE L,
V\\\$DATABASE D,
V\\\$INSTANCE I;" | tail -n 1
		)" 2>&1
	# 写入全局信息
	sysinfo["lic_version"]="${lic_version}"
	sysinfo["lic_series_no"]="${series_no}"
	sysinfo["lic_server_series"]="${server_series}"
	sysinfo["lic_server_type"]="${server_type}"
	sysinfo["lic_project_name"]="${project_name}"
	sysinfo["lic_expire"]="${expired_date}"
	sysinfo["lic_authorized_customer"]="${authorized_customer}"
	sysinfo["lic_authorized_user_number"]="${authorized_user_number}"
	sysinfo["lic_concurrency_user_number"]="${concurrency_user_number}"
	sysinfo["lic_cpu_count"]="${max_cpu_num}"
	sysinfo["lic_cpu_cores"]="${max_core_num}"
	sysinfo["lic_cluster_type"]="${cluster_type}"
	sysinfo["db_create_time"]="${db_create_time}"
	sysinfo["db_startup_time"]="${db_startup_time}"
}
#========================================================
# 函数：db_pri_backup
# 作用：对数据守护/raft 主库执行脱机全量备份
#========================================================
function db_pri_backup() {
	local bak_path="$backup_dir/$db_name/BACKUP_FILE"
	local ini_path="$data_dir/$db_name/dm.ini"
	local stop_cmd="$DMS_STOP_CMD"
	local wait_time=1
	# 打印日志，标识函数开始
	log_print "数据守护/raft 主库脱机备份"
	# 关闭主库进程
	if pgrep -f "$ini_path" &>/dev/null; then
		color_printf yellow "数据库进程存在，尝试关闭主数据库..."
		printf "\n"
		run_cmd su - dmdba -c "$stop_cmd"
		printf "\n"
		while pgrep -f "$ini_path" &>/dev/null; do sleep "$wait_time"; done
	else
		color_printf green "数据库进程不存在，无需关闭"
	fi
	# 执行脱机全量备份
	[[ -d $bak_path ]] && run_cmd mv "$bak_path" "${bak_path}${current}"
	local cmd="$env_dm_home/bin/dmrman use_ap=2 CTLSTMT=\"BACKUP DATABASE '$ini_path' FULL TO BACKUP_FILE1 BACKUPSET '$bak_path'\""
	run_cmd su - dmdba -c "$cmd"
	# 展示备份文件列表
	printf "\n"
	color_printf blue "主库查看脱机备份文件："
	printf "\n"
	run_cmd ls -la "$bak_path"
	# 上传备份到远程 SFTP
	[[ -n $sftp_server_ip ]] && sftp_cmd "$sftp_server_ip" "$sftp_server_port" "$sftp_username" "$sftp_passwd" "$sftp_dir" "$bak_path" "UP"
	# 记录备份命令到全局信息
	sysinfo[dm_bak]="$cmd"
}
#========================================================
# 函数：db_std_restore
# 作用：在数据守护/raft 备库上执行脱机恢复
#========================================================
function db_std_restore() {
	local bak_path="$backup_dir/$db_name/BACKUP_FILE"
	local ini_path="$data_dir/$db_name/dm.ini"
	local wait_time=1
	# 打印日志，标识函数开始
	log_print "数据守护/raft 备库脱机恢复"
	# 关闭备库进程
	if pgrep -f "$ini_path" &>/dev/null; then
		color_printf yellow "数据库进程存在，尝试关闭备数据库..."
		printf "\n"
		run_cmd su - dmdba -c "$DMS_STOP_CMD"
		printf "\n"
		while pgrep -f "$ini_path" &>/dev/null; do sleep "$wait_time"; done
	else
		color_printf green "数据库进程不存在，无需关闭"
	fi
	# 准备备份目录
	[[ -d $bak_path ]] && run_cmd mv "$bak_path" "${bak_path}${current}"
	# 从远程 SFTP 或主节点取回备份
	if [[ -n $sftp_server_ip ]]; then
		run_cmd mkdir -p -m 755 "$bak_path"
		sftp_cmd "$sftp_server_ip" "$sftp_server_port" "$sftp_username" "$sftp_passwd" "$sftp_dir" "$bak_path" "DOWN"
	else
		exec_scp_cmd "$bak_path" "${all_pubip_arr[0]}" "$bak_path" "DOWN"
	fi
	run_cmd chown -R dmdba:dinstall "$backup_dir"
	# 执行数据库还原
	local restore_cmd="$env_dm_home/bin/dmrman use_ap=2 CTLSTMT=\"RESTORE DATABASE '$ini_path' FROM BACKUPSET '$bak_path'\""
	run_cmd su - dmdba -c "$restore_cmd"
	printf "\n"
	# 执行数据库恢复并更新 DB_MAGIC
	local recover_cmd="$env_dm_home/bin/dmrman use_ap=2 CTLSTMT=\"RECOVER DATABASE '$ini_path' UPDATE DB_MAGIC\""
	run_cmd su - dmdba -c "$recover_cmd"
	# 记录还原与恢复命令到全局信息
	sysinfo[dm_bak]="$restore_cmd"$'\n'"$recover_cmd"
}
#========================================================
# 函数：db_dmini
# 作用：根据主/备角色修改 dm.ini 关键参数
#========================================================
function db_dmini() {
	# 定义 dm.ini 配置文件路径
	local ini="$data_dir/$db_name/dm.ini"
	local tmp
	# 创建一个临时文件
	tmp=$(mktemp)
	# 打印日志，根据节点编号区分主库和备库
	[[ $node_num -eq 1 ]] && log_print "主库配置 dm.ini" || log_print "备库配置 dm.ini"
	# 提示正在更新 dm.ini
	color_printf blue "正在更新 dm.ini ..."
	printf "\n"
	# 构建通用 sed 命令数组（支持前导空格）
	local -a sed_cmd=()
	local -a grep_pat=()
	# 根据安装模式设置不同的配置
	if [[ $dm_install_mode == "datawatch" ]]; then
		# 如果是 "datawatch" 模式，设置特定的 sed 命令
		sed_cmd=(
			"s/^[[:space:]]*ALTER_MODE_STATUS[[:space:]]*=.*/ALTER_MODE_STATUS = 1/"
			"s/^[[:space:]]*ENABLE_OFFLINE_TS[[:space:]]*=.*/ENABLE_OFFLINE_TS = 2/"
			"s/^[[:space:]]*MAL_INI[[:space:]]*=.*/MAL_INI = 1/"
			"s/^[[:space:]]*ARCH_INI[[:space:]]*=.*/ARCH_INI = 1/"
		)
		# 需要检查的配置项
		grep_pat+=(ALTER_MODE_STATUS ENABLE_OFFLINE_TS MAL_INI ARCH_INI)
		# 如果公共 IP 存在并且满足特定条件，追加 TIMER_INI 配置项
		if [[ -n $as_public_ip && ($os_flag_value -eq 0 || $node_num -eq 1) ]]; then
			sed_cmd+=("s/^[[:space:]]*TIMER_INI[[:space:]]*=.*/TIMER_INI = 1/")
			grep_pat+=(TIMER_INI)
		fi
	else
		# 如果是其他模式，设置默认的 sed 命令
		sed_cmd=(
			"s/^[[:space:]]*ALTER_MODE_STATUS[[:space:]]*=.*/ALTER_MODE_STATUS = 0/"
			"s/^[[:space:]]*ENABLE_OFFLINE_TS[[:space:]]*=.*/ENABLE_OFFLINE_TS = 2/"
			"s/^[[:space:]]*ARCH_INI[[:space:]]*=.*/ARCH_INI = 1/"
		)
		# 需要检查的配置项
		grep_pat=(ALTER_MODE_STATUS ENABLE_OFFLINE_TS ARCH_INI)
	fi
	# 一次性执行所有 sed 命令，将更改应用到配置文件
	printf '%s\n' "${sed_cmd[@]}" | run_cmd su - dmdba -c "sed -i -f - $ini"
	# 收集并保存修改结果，执行 grep 查找并打印修改的配置项
	run_cmd su - dmdba -c "grep -E '$(
		IFS='|'
		echo "${grep_pat[*]}"
	)' $ini" | tee "$tmp"
	# 将修改后的结果保存到 sysinfo 数组中
	sysinfo[dmIni]="$(escape_string "$(sed <"$tmp" 's/^\t*//')")"
}
#========================================================
# 函数：dw_dmmal
# 作用：生成并写入 dmmal.ini，配置 MAL 通信
#========================================================
function dw_dmmal() {
	# 打印角色区分提示
	printf "\n"
	if [[ $node_num -eq 1 ]]; then
		color_printf blue "主库配置 dmmal.ini 参数："
	else
		color_printf blue "备库配置 dmmal.ini 参数："
	fi
	printf "\n"
	# 生成 dmmal.ini 基础配置
	local config
	config=$(
		cat <<EOF
MAL_CHECK_INTERVAL = 5
MAL_CONN_FAIL_INTERVAL = 15
MAL_TEMP_PATH = ${data_dir}/${db_name}
MAL_BUF_SIZE = 512
MAL_SYS_BUF_SIZE = 2048
MAL_COMPRESS_LEVEL = 0
MAL_VPOOL_SIZE = 128
EOF
	)
	# 计算节点总数并检查越界
	local node_counts=$((${#db_pubip_arr[@]} + ${#as_pubip_arr[@]} + ${#sy_pubip_arr[@]}))
	# 循环追加 MAL_INST 配置
	for ((i = 0; i < node_counts; i++)); do
		local instance_num=$((i + 1)) mal_link_magic
		# 两地三中心时使用业务网络作为心跳网络
		[[ "${all_pubip_arr[$i]}" == "${all_malip_arr[$i]}" ]] && mal_link_magic=1 || mal_link_magic=0
		config+=$(
			cat <<EOF

[MAL_INST$instance_num]
MAL_INST_NAME = ${instance_name%??}$(printf "%02d" $instance_num)
MAL_HOST = ${all_malip_arr[$i]}
MAL_PORT = $mal_port_num
MAL_INST_HOST = ${all_pubip_arr[$i]}
MAL_INST_PORT = $port_num
MAL_DW_PORT = $mal_dw_port_num
MAL_INST_DW_PORT = $mal_inst_dw_port_num
MAL_LINK_MAGIC = ${mal_link_magic}
EOF
		)
	done
	# 一次性写入 dmmal.ini
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$data_dir/$db_name/dmmal.ini"
EOF
	# 保存配置到全局信息
	sysinfo["dmmalIni"]="$(escape_string "$(cat "$data_dir/$db_name/dmmal.ini")")"
}
#========================================================
# 函数：db_dmarch
# 作用：根据主/备/异步/同步角色生成 dmarch.ini
#========================================================
function db_dmarch() {
	# 打印角色区分提示
	printf "\n"
	if [[ $node_num -eq 1 ]]; then
		color_printf blue "主库配置 dmarch.ini 参数："
	else
		color_printf blue "备库配置 dmarch.ini 参数："
	fi
	printf "\n"
	local config
	# 数据守护模式 dmarch.ini 配置
	if [[ $dm_install_mode == "datawatch" ]]; then
		config=$(
			cat <<EOF
ARCH_WAIT_APPLY = $arch_wait_apply
[ARCHIVE_LOCAL1]
ARCH_TYPE = LOCAL
ARCH_DEST = $arch_dir/$db_name
ARCH_FILE_SIZE = $log_size
ARCH_SPACE_LIMIT = $space_limit
EOF
		)
		# 计算归档起始序号
		local x=0
		if [ ${#db_pubip_arr[@]} -eq 1 ] && { [ -n "$as_public_ip" ] || [ -n "$sy_public_ip" ]; }; then
			x=1
		fi
		# 配置实时主备归档
		if [[ -n "$db_public_ip" && $os_flag_value -eq 0 ]]; then
			arch_name_seq=1
			for ((i = x; i < ${#db_pubip_arr[@]}; i++)); do
				if [[ $i -ne $((node_num - 1)) ]]; then
					arch_dest="${instance_name%??}$(printf "%02d" $((i + 1)))"
					config+=$(
						cat <<EOF

[ARCHIVE_${dw_arch_type}${arch_name_seq}]
ARCH_TYPE = $dw_arch_type
ARCH_DEST = $arch_dest
EOF
					)
					((arch_name_seq++))
				fi
			done
		fi
		# 配置异步主备归档
		if [[ $os_flag_value -eq 0 || $node_num -eq 1 ]]; then
			if [[ -n "$as_public_ip" ]]; then
				for ((i = 0; i < ${#as_pubip_arr[@]}; i++)); do
					arch_dest="${instance_name%??}$(printf "%02d" $((i + 1 + ${#db_pubip_arr[@]})))"
					config+=$(
						cat <<EOF

[ARCHIVE_ASYNC$((i + 1))]
ARCH_TYPE = ASYNC
ARCH_DEST = $arch_dest
ARCH_TIMER_NAME = RT_TIMER$((i + 1))
ARCH_SEND_DELAY = 0
EOF
					)
				done
			fi
			# 配置同步主备归档
			if [[ -n "$sy_public_ip" ]]; then
				for ((i = 0; i < ${#sy_pubip_arr[@]}; i++)); do
					arch_dest="${instance_name%??}$(printf "%02d" $((i + 1 + ${#db_pubip_arr[@]} + ${#as_pubip_arr[@]})))"
					config+=$(
						cat <<EOF

[ARCHIVE_SYNC$((i + 1))]
ARCH_TYPE = SYNC
ARCH_DEST = $arch_dest
ARCH_RECOVER_TIME = ${arch_recover_time}
EOF
					)
				done
			fi
		fi
	else
		# raft 模式 dmarch.ini 配置
		config=$(
			cat <<EOF
RAFT_SELF_ID = $((node_num - 1))
RAFT_HB_INTERVAL = ${raft_hb_interval}
RAFT_VOTE_INTERVAL = $((raft_vote_interval + (node_num - 1) * 2000))
XMAL_HB_INTERVAL = ${xmal_hb_interval}
XMAL_IP = ${all_malip_arr[((node_num - 1))]}
XMAL_PORT = ${xmal_port}
[ARCHIVE_LOCAL1]
ARCH_TYPE = LOCAL
ARCH_DEST = $arch_dir/$db_name
ARCH_FILE_SIZE = $log_size
ARCH_SPACE_LIMIT = $space_limit
EOF
		)

		# 追加 raft 归档配置
		arch_name_seq=1
		for ((i = 0; i < ${#db_pubip_arr[@]}; i++)); do
			if [[ $i -ne $((node_num - 1)) ]]; then
				arch_dest="${instance_name%??}$(printf "%02d" $((i + 1)))"
				config+=$(
					cat <<EOF

[ARCHIVE_EP${arch_name_seq}]
ARCH_TYPE = RAFT
ARCH_DEST = $arch_dest
ARCH_DEST_IP = ${all_malip_arr[$i]}
ARCH_DEST_PORT = ${arch_dest_port}
ARCH_DEST_ID = $i
EOF
				)
				((arch_name_seq++))
			fi
		done
	fi
	# 一次性写入 dmarch.ini
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$data_dir/$db_name/dmarch.ini"
EOF
	# 保存配置到全局信息
	sysinfo["dmarchIni"]="$(escape_string "$(cat "$data_dir/$db_name/dmarch.ini")")"
}
#========================================================
# 函数：dw_timer
# 作用：为异步备库生成 dmtimer.ini
#========================================================
function dw_timer() {
	# 判断是否需要创建定时器
	if [[ -n "$as_public_ip" ]] && { [ "$os_flag_value" -eq 0 ] || [ "$node_num" -eq 1 ]; }; then
		# 打印角色区分提示
		printf "\n"
		if [[ $node_num -eq 1 ]]; then
			color_printf blue "主库配置 dmtimer.ini 参数："
		else
			color_printf blue "备库配置 dmtimer.ini 参数："
		fi
		# 计算异步节点数量
		local c=${#as_pubip_arr[@]}
		# 拼接 dmtimer.ini 内容
		local config
		for ((i = 0; i < c; i++)); do
			config+=$(
				cat <<EOF

[RT_TIMER$((i + 1))]
TYPE = 10
FREQ_MONTH_WEEK_INTERVAL =
FREQ_SUB_INTERVAL =
REPEAT_INTERVAL= FREQ=SECONDLY;INTERVAL= ${repeat_interval}
START_TIME = 01:00:00
END_TIME =
DURING_START_DATE = 2016-02-11 17:36:09
DURING_END_DATE = 9999-12-31 23:59:59
NO_END_DATE_FLAG = 1
DESCRIBE = RT_TIMER$((i + 1))
IS_VALID = 1
EOF
			)
		done
		# 一次性写入 dmtimer.ini
		run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee -a "$data_dir/$db_name/dmtimer.ini"
EOF
		# 保存配置到全局信息
		sysinfo["dmtimerIni"]="$(escape_string "$(cat "$data_dir/$db_name/dmtimer.ini")")"
	fi
}
#========================================================
# 函数：dw_dmwatcher
# 作用：生成并注册 dmwatcher.ini
#========================================================
function dw_dmwatcher() {
	# 打印角色区分提示
	printf "\n"
	if [[ $node_num -eq 1 ]]; then
		color_printf blue "主库配置 dmwatcher.ini 参数："
	else
		color_printf blue "备库配置 dmwatcher.ini 参数："
	fi
	printf "\n"
	# 判断是否为本地守护模式
	local condition_true=false
	if { [ -n "$as_public_ip" ] || [ -n "$sy_public_ip" ]; } && { [ "$os_flag_value" -eq 1 ] || [ "$os_flag_value" -eq 2 ] || [ ${#db_pubip_arr[@]} -eq 1 ]; }; then
		condition_true=true
	fi
	# 生成 dmwatcher.ini 基础配置
	local config
	config=$(
		cat <<EOF
[$dw_group_name]
DW_TYPE = GLOBAL
DW_MODE = $dw_mode
DW_ERROR_TIME = 30
INST_ERROR_TIME = 20
INST_RECOVER_TIME = 60
INST_OGUID = $oguid
INST_INI = $data_dir/$db_name/dm.ini
INST_AUTO_RESTART = 1
INST_STARTUP_CMD = $DMS_START_CMD
RLOG_SEND_THRESHOLD = 0
RLOG_APPLY_THRESHOLD = 0
EOF
	)
	# 若为本地守护模式，修改 DW_TYPE 与 DW_MODE
	if [[ $condition_true == "true" ]]; then
		config=$(echo "$config" | sed -e 's/DW_TYPE[[:space:]]*=[[:space:]]*GLOBAL/DW_TYPE = LOCAL/g' -e 's/DW_MODE[[:space:]]*=[[:space:]]*AUTO/DW_MODE = MANUAL/g')
	fi
	# 一次性写入 dmwatcher.ini
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$data_dir/$db_name/dmwatcher.ini"
EOF
	# 打印注册提示
	printf "\n"
	if [[ $node_num -eq 1 ]]; then
		color_printf blue "主库注册 dmwatcher 服务："
	else
		color_printf blue "备库注册 dmwatcher 服务："
	fi
	printf "\n"
	# 注册新守护服务
	run_cmd "$env_dm_home"/script/root/dm_service_installer.sh -t dmwatcher -watcher_ini "$data_dir"/"$db_name"/dmwatcher.ini -p "$dw_group_name"
	# 保存配置到全全局信息
	sysinfo["dmwatcherIni"]="$(escape_string "$(cat "$data_dir/$db_name/dmwatcher.ini")")"
}
#========================================================
# 函数：dw_monitor
# 作用：生成并注册监视器配置
#========================================================
function dw_monitor() {
	# 根据节点号决定 MON_DW_CONFIRM 值
	local dw_confirm path
	if [[ $node_num -eq -1 ]]; then
		dw_confirm=1
		path=$env_dm_home/data
	else
		dw_confirm=0
		path=$data_dir
	fi
	# 打印角色区分提示
	case $node_num in
	1)
		printf "\n"
		color_printf blue "主库配置 dmmonitor.ini 参数："
		printf "\n"
		;;
	-1)
		log_print "配置确认监视器"
		;;
	*)
		printf "\n"
		color_printf blue "备库配置 dmmonitor.ini 参数："
		printf "\n"
		;;
	esac
	# 生成 dmmonitor.ini 基础配置
	local config
	config=$(
		cat <<EOF
MON_DW_CONFIRM = $dw_confirm
MON_LOG_PATH = $env_dm_home/log
MON_LOG_INTERVAL = 60
MON_LOG_FILE_SIZE = 32
MON_LOG_SPACE_LIMIT = 2048
[$dw_group_name]
MON_INST_OGUID = $oguid
EOF
	)
	# 循环追加 MON_DW_IP 配置
	for ((i = 0; i < ${#db_pubip_arr[@]} + ${#as_pubip_arr[@]} + ${#sy_pubip_arr[@]}; i++)); do
		config+=$(
			cat <<EOF

MON_DW_IP = ${all_malip_arr[$i]}:$mal_dw_port_num
EOF
		)
	done
	# 一次性写入 dmmonitor.ini
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$path/$db_name/dmmonitor_${dw_group_name}.ini"
EOF
	# 若为确认监视器，注册并启动服务
	if [[ $dw_confirm -eq 1 ]]; then
		# 打印注册提示
		printf "\n"
		color_printf blue "监视主机注册监视器服务："
		printf "\n"
		# 注册新监视器服务
		run_cmd "$env_dm_home"/script/root/dm_service_installer.sh -t dmmonitor -monitor_ini "$path/$db_name/dmmonitor_${dw_group_name}.ini" -p "$dw_group_name"
		# 启动监视进程
		printf "\n"
		color_printf blue "监视主机启动监视进程："
		printf "\n"
		run_cmd su - dmdba -c "$DMM_START_CMD"
	fi
	# 保存配置到全局信息
	sysinfo["dmmonitorIni"]="$(escape_string "$(cat "$path/$db_name/dmmonitor_${dw_group_name}.ini")")"
}
#========================================================
# 函数：db_startup
# 作用：启动数据库到 MOUNT 并设置主/备模式
#========================================================
function db_startup() {
	local WAIT_TIME="1s"
	# 打印角色区分日志
	if [[ $node_num -eq 1 ]]; then
		log_print "数据守护[raft主备]主库启动"
		color_printf blue "主库启动到 mount 状态："
	else
		log_print "数据守护[raft主备]备库启动"
		color_printf blue "备库启动到 mount 状态："
	fi
	printf "\n"
	# 启动数据库到 MOUNT
	run_cmd su - dmdba -c "$DMS_START_CMD mount"
	# 若为 raft 模式直接返回
	[[ $dm_install_mode == "raft" ]] && return
	# 等待数据库进入 MOUNT 状态
	check_db_status "MOUNT"
	# 生成并执行主/备模式 SQL 脚本
	printf "\n"
	_generate_sql_script() {
		local script_name="$1"
		local db_mode="$2"
		local config
		config=$(
			cat <<EOF
set echo off
set timing off
set feedback off
set lineshow off
sp_set_oguid($oguid);
alter database $db_mode;
SP_SET_PARA_VALUE(1, 'ALTER_MODE_STATUS', 0);
exit;
EOF
		)
		run_cmd su - dmdba <<EOF
printf "%s" "$config" | tee "${scripts_dir}/${script_name}" >/dev/null
EOF
	}
	if [[ $node_num -eq 1 ]]; then
		color_printf blue "主库修改模式："
		printf "\n"
		_generate_sql_script "dw_conf_pri.sql" "primary"
		execute_script "SYSDBA" "$sysdba_pwd_temp" "dw_conf_pri.sql"
		sysinfo["dm_mode"]="$(escape_string "$(cat ${scripts_dir}/dw_conf_pri.sql)")"
	else
		color_printf blue "备库修改模式："
		printf "\n"
		_generate_sql_script "dw_conf_std.sql" "standby"
		execute_script "SYSDBA" "$sysdba_pwd_temp" "dw_conf_std.sql"
		sysinfo["dm_mode"]="$(escape_string "$(cat ${scripts_dir}/dw_conf_std.sql)")"
	fi
	# 启动守护进程
	printf "\n"
	if [[ $node_num -eq 1 ]]; then
		color_printf blue "主库启动守护进程："
	else
		color_printf blue "备库启动守护进程："
	fi
	printf "\n"
	run_cmd su - dmdba -c "$DMW_START_CMD"
}
#========================================================
# 函数：dsc_multipath_rules
# 作用：根据磁盘列表生成 multipath.conf 及 udev 规则
#========================================================
function dsc_multipath_rules() {
	# 内部工具 - 获取设备 WWID/UUID
	_uuid() { get_wwid "$1"; }
	# 内部工具 - 按类型生成单行规则
	# $1: 规则类型  $2: 设备路径  $3: 符号链接名
	_rule_line() {
		local t=$1 dev=$2 link=$3 uuid
		uuid=$(_uuid "$dev")
		case $t in
		multipath)
			printf "  multipath {\n    wwid   \"%s\"\n    alias  %s\n  }\n" "$uuid" "$link"
			;;
		symlink)
			[[ $dev =~ ^/dev ]] && uuid="mpath-$uuid"
			printf "KERNEL==\"dm-*\", ENV{DM_UUID}==\"%s\", SYMLINK+=\"asmdisk/%s\", OWNER=\"dmdba\", GROUP=\"dinstall\", MODE=\"0660\"\n" "$uuid" "$link"
			;;
		scsi)
			printf "KERNEL==\"sd*|vd*\", BUS==\"scsi\", PROGRAM==\"/sbin/scsi_id -g -u -d /dev/\$name\", RESULT==\"%s\", SYMLINK+=\"asmdisk/%s\", OWNER=\"dmdba\", GROUP=\"dinstall\", MODE=\"0660\"\n" "$uuid" "$link"
			;;
		block)
			printf "KERNEL==\"sd*|vd*\", SUBSYSTEM==\"block\", PROGRAM==\"/usr/lib/udev/scsi_id -g -u -d /dev/\$name\", RESULT==\"%s\", SYMLINK+=\"asmdisk/%s\", OWNER=\"dmdba\", GROUP=\"dinstall\", MODE=\"0660\"\n" "$uuid" "$link"
			;;
		nvme)
			printf "KERNEL==\"nvme*\", ENV{ID_WWN}==\"%s\", SYMLINK+=\"asmdisk/%s\", OWNER=\"dmdba\", GROUP=\"dinstall\", MODE=\"0660\"\n" "$uuid" "$link"
			;;
		device)
			printf "KERNEL==\"sd*|vd*|nvme*\", ENV{ID_PART_ENTRY_UUID}==\"%s\", SYMLINK+=\"asmdisk/%s\", OWNER=\"dmdba\", GROUP=\"dinstall\", MODE=\"0660\"\n" "$uuid" "$link"
			;;
		esac
	}
	# 内部工具 - 遍历 disk_arr 生成规则
	# $1: 规则类型
	_gen_rules() {
		local rule_type=$1 x y
		for ((x = 0; x < ${#disk_arr[@]}; x++)); do
			y=1
			for i in ${disk_arr[$x]//,/ }; do
				_rule_line "$rule_type" "$(lower "$i")" "${asm_disk_arr[$x]}$(printf "%02d" "$y")"
				((y++))
			done
		done
	}
	# 主流程 - 初始化规则内容变量
	local rules_content=""
	# 若启用 multipath，生成 multipath.conf
	if [[ $multipath == "Y" ]]; then
		if [[ ${disk_arr[0]:0:4} == "/dev" ]]; then
			local vendor product device_name devbase
			# 提取设备名（支持 sdX / nvmeXnY）
			devbase=${disk_arr[0]##*/}
			if [[ $devbase =~ ^nvme[0-9]+n[0-9]+ ]]; then
				device_name=${devbase%%p[0-9]*}
			else
				device_name=${devbase%%[0-9]*}
			fi
			# 读取厂商信息
			vendor_file="/sys/class/block/$device_name/device/vendor"
			model_file="/sys/class/block/$device_name/device/model"
			vendor=$([[ -r $vendor_file ]] && xargs <"$vendor_file" || echo "Generic")
			product=$([[ -r $model_file ]] && xargs <"$model_file" || echo "Storage Device")

			rules_content=$(
				cat <<EOF
defaults {
  polling_interval        30
  failback                immediate
  no_path_retry           5
  rr_min_io               100
  path_checker            tur
  user_friendly_names     yes
}

blacklist {
  devnode "^(ram|raw|loop|fd|md|dm-|sr|scd|st)[0-9]*"
  devnode "^(hd|xvd)[a-z]*"
}

multipaths {
$(_gen_rules multipath)
}

devices {
  device {
    vendor "$vendor"
    product "$product"
    path_grouping_policy group_by_prio
    prio const
  }
}
EOF
			)
			printf "%s\n" "$rules_content" >"$MULTIPATH_CONF_PATH"
			printf "\e[34m配置 %s：\e[0m\n\n%s\n" "$MULTIPATH_CONF_PATH" "$rules_content"
		fi
		# 生成 udev 符号链接规则
		_gen_rules symlink >"$dm_rules"
	else
		# 非 multipath 场景，按 OS 版本选择规则模板
		local rule_type
		if [[ $os_version -eq 6 ]]; then
			rule_type=scsi
		else
			rule_type=block
			if [[ "${disk_arr[0]}" =~ nvme ]]; then
				rule_type=nvme
				[[ "${disk_arr[0]}" =~ p[0-9]+$ ]] && rule_type=device
			else
				[[ "${disk_arr[0]}" =~ [0-9]+$ ]] && rule_type=device
			fi
		fi
		_gen_rules "$rule_type" >"$dm_rules"
	fi
}
#========================================================
# 函数：dsc_conf_disk_arr
# 作用：创建或分发 ASM 磁盘（操作系统级别）的 udev 规则
#========================================================
function dsc_conf_disk_arr() {
	# 打印操作类型日志
	log_print "$([ "$node_num" -gt 1 ] && echo "拷贝" || echo "创建") ASM 磁盘(操作系统级别)配置文件"
	# 确定规则文件路径
	local dm_rules
	dm_rules="$UDEV_RULES_PATH/$([ "$multipath" = "Y" ] && echo "$DM_PERMISSIONS_RULES" || echo "$DM_ASMDEVICES_RULES")"
	# 备份并清空旧规则
	bak_file "$dm_rules"
	: >"$dm_rules"
	# 首节点安装 multipath 软件（如需要）
	if [[ $multipath == "Y" && ${disk_arr[0]:0:4} == "/dev" ]]; then
		run_cmd mpathconf --enable --with_multipathd y
	fi
	# 首节点生成规则，其余节点拷贝
	if ((node_num == 1)); then
		dsc_multipath_rules >"$dm_rules"
	else
		if [[ $multipath == "Y" && ${disk_arr[0]:0:4} == "/dev" ]]; then
			exec_scp_cmd "$MULTIPATH_CONF_PATH" "${all_pubip_arr[0]}" "$MULTIPATH_CONF_PATH" "DOWN"
		fi
		exec_scp_cmd "$dm_rules" "${all_pubip_arr[0]}" "$dm_rules" "DOWN"
	fi
	# 判断是否配置聚合磁盘
	if [[ $multipath == "Y" && ${disk_arr[0]:0:4} == "/dev" ]]; then
		sysinfo[multipathConf]="$(escape_string "$(cat "$MULTIPATH_CONF_PATH")")"
		printf "\e[34m配置 %s：\e[0m\n\n%s\n\n" "$MULTIPATH_CONF_PATH" "$(cat "$MULTIPATH_CONF_PATH")"
	fi
	sysinfo[dmRules]="$(escape_string "$(cat "$dm_rules")")"
	# 确保规则文件权限为 644，防止权限问题
	chmod 644 "$dm_rules"
	# 打印最终规则内容
	printf "\e[34m配置 %s：\e[0m\n\n%s\n" "$dm_rules" "$(cat "$dm_rules")"
}
#========================================================
# 函数：dsc_start_udev
# 作用：使 multipath 及 udev 规则生效
#========================================================
function dsc_start_udev() {
	local WAIT_TIME=5
	# 若启用 multipath，重启并刷新聚合磁盘
	if [[ $multipath == "Y" && ${disk_arr[0]:0:4} == "/dev" ]]; then
		log_print "查看 multipath 聚合磁盘"
		if ((os_version == 6)); then
			service multipathd restart >/dev/null 2>&1
			chkconfig multipathd on >/dev/null 2>&1
		else
			systemctl restart multipathd -q
			systemctl enable multipathd -q
		fi
		# 清除现有 multipath 配置
		multipath -F >/dev/null 2>&1
		multipath -r >/dev/null 2>&1
		# 等待multipath重建完成
		while :; do
			if multipath -ll -v1 2>/dev/null | grep -qE "${asm_disk_arr[@]: -1}"; then
				break
			fi
			sleep 1
		done
		# 所有设备就绪，执行最终 multipath -ll（可选）
		multipath -ll
	fi
	# 重载并触发 udev 规则，直到检测到 ASM 磁盘
	log_print "查看 udev 磁盘"
	while :; do
		if ((os_version == 6)); then
			printf "\n"
			start_udev
		else
			udevadm control --reload-rules
			udevadm trigger --type=devices --action=change
		fi
		sleep "$WAIT_TIME"
		if find "$ASM_DEVICE_PATH" -mindepth 1 -name 'dm*' -print -quit | grep -q .; then
			ls -lh "$ASM_DEVICE_PATH"
			break
		fi
	done
}
#========================================================
# 函数：dsc_dmdcr_cfg
# 作用：生成或拷贝 dmdcr_cfg.ini（DCR 配置）
#========================================================
function dsc_dmdcr_cfg() {
	local cfg_ini="$data_dir/$dsc_db_name/dmdcr_cfg.ini"
	local ep_count=${#all_pubip_arr[@]}
	local config
	# 写入基础配置
	_write_basic_cfg() {
		if [[ $dm_install_mode == "dsc" ]]; then
			config=$(
				cat <<EOF
DCR_N_GRP = 3
DCR_VTD_PATH = $ASM_DEVICE_PATH/${asm_disk_arr[1]}01
DCR_OGUID = $oguid
EOF
			)
		else
			config=$(
				cat <<EOF
DCR_N_GRP = 3
DCR_DISK_LOAD_PATH = $ASM_DEVICE_PATH
DCR_OGUID = $oguid
EOF
			)
		fi
	}
	# 定义各组通用信息
	local -A groups=(
		["CSS"]="DCR_GRP_TYPE = CSS
DCR_GRP_NAME = GRP_CSS
DCR_GRP_N_EP = $ep_count
DCR_GRP_DSKCHK_CNT = 60"
		["ASM"]="DCR_GRP_TYPE = ASM
DCR_GRP_NAME = GRP_ASM
DCR_GRP_N_EP = $ep_count
DCR_GRP_DSKCHK_CNT = 60"
		["DB"]="DCR_GRP_TYPE = DB
DCR_GRP_NAME = GRP_${dsc_db_name}
DCR_GRP_N_EP = $ep_count
DCR_GRP_DSKCHK_CNT = 60"
	)
	# 写入组通用信息
	_write_group_info() {
		local group=$1
		local group_info="${groups[$group]}"
		group_info=$(eval "echo \"$group_info\"")
		if [[ -n "$group_info" ]]; then
			config+=$(
				cat <<EOF

[GRP]
$group_info
EOF
			)
		fi
	}
	# 写入各组端点信息
	_write_endpoints() {
		local group_name=$1
		for ((x = 1; x <= ep_count; x++)); do
			local ep_host="${malip_host_arr[$((x - 1))]}"
			local ep_name
			ep_name="${group_name}$(printf "%02d" "$x")"
			case $group_name in
			"CSS")
				config+=$(
					cat <<EOF

[GRP_CSS]
DCR_EP_NAME = $ep_name
DCR_EP_HOST = $ep_host
DCR_EP_PORT = $css_dcr_ep_port
EOF
				)
				;;
			"ASM")
				config+=$(
					cat <<EOF

[GRP_ASM]
DCR_EP_NAME = $ep_name
DCR_EP_SHM_KEY = 789$(printf "%02d" "$x")
DCR_EP_SHM_SIZE = 512
DCR_EP_HOST = $ep_host
DCR_EP_PORT = $asm_dcr_ep_port
EOF
				)
				if [[ $dm_install_mode == "dsc" ]]; then
					config+=$(
						cat <<EOF

DCR_EP_ASM_LOAD_PATH = $ASM_DEVICE_PATH
EOF
					)
				fi
				;;
			"DB")
				config+=$(
					cat <<EOF

[GRP_${dsc_db_name}]
DCR_EP_NAME = $(upper "${dsc_ep_name}$(printf "%02d" "$x")")
DCR_EP_SEQNO = $((x - 1))
DCR_EP_PORT = $port_num
DCR_CHECK_PORT = $db_dcr_ep_port
EOF
				)
				;;
			esac
		done
	}
	# 首节点创建配置文件
	_create_config_file() {
		_write_basic_cfg
		for group in CSS ASM DB; do
			_write_group_info "$group"
			_write_endpoints "$group"
		done
		run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$cfg_ini"
EOF
	}
	# 非首节点拷贝配置文件
	_copy_config_file() {
		exec_scp_cmd "$cfg_ini" "${all_pubip_arr[0]}" "$cfg_ini" "DOWN"
		run_cmd chown -R dmdba:dinstall "$cfg_ini"
		cat "$cfg_ini"
	}
	# 主逻辑
	if [[ $node_num -eq 1 ]]; then
		log_print "配置 dmdcr_cfg.ini"
		_create_config_file
	else
		log_print "拷贝 dmdcr_cfg.ini"
		_copy_config_file
	fi
	# 添加配置文件到关联数组
	sysinfo[cfgIni]="$(escape_string "$(cat "$cfg_ini")")"
}
#========================================================
# 函数：dsc_dmasvmal
# 作用：生成或拷贝 dmasvrmal.ini（ASM MAL 通信配置）
#========================================================
function dsc_dmasvmal() {
	log_print "配置dmasvrmal.ini"
	local dmasvmal_ini="$data_dir/$dsc_db_name/dmasvrmal.ini"
	# 首节点生成配置
	if [[ $node_num -eq 1 ]]; then
		local config
		for ((x = 0; x < ${#all_pubip_arr[@]}; x++)); do
			local mal_host ep_name
			ep_name="ASM$(printf "%02d" $((x + 1)))"
			mal_host="${malip_host_arr[$x]}"
			config+=$(
				cat <<EOF

[MAL_INST$((x + 1))]
MAL_INST_NAME = $ep_name
MAL_HOST      = $mal_host
MAL_PORT      = $asvrmal_mal_port
EOF
			)
		done
		run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | sed '/^$/d' | tee "$dmasvmal_ini"
EOF
	else
		# 非首节点拷贝
		exec_scp_cmd "$dmasvmal_ini" "${all_pubip_arr[0]}" "$dmasvmal_ini" "DOWN"
		run_cmd chown dmdba:dinstall "$dmasvmal_ini"
		cat "$dmasvmal_ini"
	fi
	# 添加配置文件到关联数组
	sysinfo[dmasvmalIni]="$(escape_string "$(cat "$dmasvmal_ini")")"
}
#========================================================
# 函数：dsc_dmdcr
# 作用：生成 dmdcr.ini（DCR 启动参数）
#========================================================
function dsc_dmdcr() {
	log_print "配置dmdcr.ini"
	local dmdcr_ini="$data_dir/$dsc_db_name/dmdcr.ini"
	local config
	# 基础路径配置
	if [[ $dm_install_mode == "dsc" ]]; then
		config="DMDCR_PATH = $ASM_DEVICE_PATH/${asm_disk_arr[0]}01"
	else
		config="DMDCR_PATH = $ASM_DEVICE_PATH"
	fi
	# 追加通用配置
	config+=$(
		cat <<EOF

DMDCR_MAL_PATH = $data_dir/$dsc_db_name/dmasvrmal.ini
DMDCR_SEQNO = $((node_num - 1))
DMDCR_ASM_RESTART_INTERVAL = 0
DMDCR_ASM_STARTUP_CMD = $ASMSVR_START_CMD
DMDCR_DB_RESTART_INTERVAL = 0
DMDCR_DB_STARTUP_CMD = $DB_START_CMD
DMDCR_AUTO_OPEN_CHECK = 111
DMDCR_ASM_TRACE_LEVEL = 2
EOF
	)
	# 写入文件
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$dmdcr_ini"
EOF
	# 若启用 link_check_ip，追加并设置 CA
	if [[ -n "$link_check_ip" ]]; then
		# 追加配置
		run_cmd su - dmdba <<EOF
printf "DMDCR_LINK_CHECK_IP = %s\n" "$link_check_ip" | tee -a "$dmdcr_ini"
EOF
		# 仅对当前模式需要的可执行文件设置 CAP
		local -A mode_map=(
			[dsc]="dmcss dmasmsvr dmserver"
			[dscm]="dmcss dmasmsvrm dmserver"
		)
		local target_files="${mode_map[$dm_install_mode]}"
		for fs in $target_files; do
			local bin_path="$env_dm_home/bin/$fs"
			if [[ -f "$bin_path" ]]; then
				run_cmd setcap cap_net_raw,cap_net_admin=eip "$bin_path"
			else
				log_print "警告: 文件 $bin_path 不存在，跳过权限设置。"
			fi
		done
	fi
	# 添加配置文件到关联数组
	sysinfo[dmdcrIni]="$(escape_string "$(cat "$dmdcr_ini")")"
}
#========================================================
# 函数：dsc_dmcssm
# 作用：生成 dmcssm.ini（CSS 监视器配置）
#========================================================
function dsc_dmcssm() {
	log_print "配置dmcssm.ini"
	local dmcssm_ini="$data_dir/$dsc_db_name/dmcssm.ini"
	local config
	config=$(
		cat <<EOF
CSSM_OGUID = $oguid
$(echo "${malip_host_arr[@]}" | awk -v port="$css_dcr_ep_port" '{for(i=1;i<=NF;i++) printf "CSSM_CSS_IP = %s:%s\n", $i, port}')
CSSM_LOG_PATH = $env_dm_home/log
CSSM_LOG_FILE_SIZE = 32
CSSM_LOG_SPACE_LIMIT = 0
EOF
	)
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee "$dmcssm_ini"
EOF
	# 添加配置文件到关联数组
	sysinfo[dmcssmIni]="$(escape_string "$(cat "$dmcssm_ini")")"
}
#========================================================
# 函数：dsc_dmsvc
# 作用：生成 /etc/dm_svc.conf（客户端连接服务名）
#========================================================
function dsc_dmsvc() {
	log_print "配置 /etc/dm_svc.conf"
	local dm_svc_file="/etc/dm_svc.conf"
	bak_file "$dm_svc_file"
	local db_conn_str
	db_conn_str=$(printf "%s:$port_num," "${all_pubip_arr[@]}" | sed 's/,$//')
	local config
	config=$(
		cat <<EOF

# DMBegin
$dsc_db_name=($db_conn_str)
[$dsc_db_name]
SWITCH_TIMES=(60)
SWITCH_INTERVAL=(1000)
EP_SELECTOR=(0)
AUTO_RECONNECT=(1)
# DMEnd
EOF
	)
	run_cmd chown dmdba:dinstall "$dm_svc_file"
	run_cmd su - dmdba <<EOF
printf "%s\n" "$config" | tee -a "$dm_svc_file" >/dev/null
EOF
	cat "$dm_svc_file"
}
#========================================================
# 函数：dsc_dmasmcmd
# 作用：使用 dmasmcmd 初始化 ASM 磁盘
#========================================================
function dsc_dmasmcmd() {
	log_print "初始化 ASM 磁盘"
	local asm_disk_file="$data_dir/$dsc_db_name/asmdisk.txt"
	local asm_script_content="#asm script file\n"
	local asm_disk_paths=""
	local name path type tool
	# 生成 create/init 语句
	for ((i = 0; i < ${#disk_arr[@]}; i++)); do
		IFS=',' read -ra disks <<<"${disk_arr[$i]}"
		for ((j = 1; j <= ${#disks[@]}; j++)); do
			name="${asm_disk_arr[i]}$(printf '%02d' $j)"
			path="$ASM_DEVICE_PATH/$name"
			if [[ $dm_install_mode == "dsc" ]]; then
				case $i in
				0) type=dcrdisk ;;
				1) type=votedisk ;;
				*) type=asmdisk ;;
				esac
			else
				type=$([[ $i -eq 0 ]] && echo dcrvdisk || echo asmdisk)
				[[ $i -eq 0 ]] && asm_disk_paths+="'$path',"
			fi
			asm_script_content+="create $type '$path' '$name'\n"
		done
	done
	# 追加 init 或 create system diskgroup
	if [[ $dm_install_mode == "dsc" ]]; then
		asm_script_content+="init dcrdisk '$ASM_DEVICE_PATH/${asm_disk_arr[0]}01' from '$data_dir/$dsc_db_name/dmdcr_cfg.ini' identified by '$asm_pwd'\n"
		asm_script_content+="init votedisk '$ASM_DEVICE_PATH/${asm_disk_arr[1]}01' from '$data_dir/$dsc_db_name/dmdcr_cfg.ini'\n"
	else
		asm_disk_paths="${asm_disk_paths%,}"
		asm_script_content+="create system diskgroup asmdisk $asm_disk_paths attribute config='$data_dir/$dsc_db_name/dmdcr_cfg.ini', passwd='$asm_pwd'\n"
	fi
	# 写入并执行脚本
	printf "%b" "$asm_script_content" >"$asm_disk_file"
	tool=$([ "$dm_install_mode" = "dsc" ] && echo "dmasmcmd" || echo "dmasmcmdm")
	chown dmdba:dinstall "$asm_disk_file"
	su - dmdba -c "$env_dm_home/bin/$tool script_file=$asm_disk_file"
	# 添加配置文件到关联数组
	sysinfo[asmDiskFile]="$(escape_string "$(cat "$asm_disk_file")")"
}
#========================================================
# 函数：dsc_dmservice
# 作用：注册 CSS 与 ASM 服务
#========================================================
function dsc_dmservice() {
	log_print "注册 CSS 和 ASM 服务"
	local dcr_ini="$data_dir/$dsc_db_name/dmdcr.ini"
	local installer="$env_dm_home/script/root/dm_service_installer.sh"
	local asm_type
	# 安装 CSS
	run_cmd "$installer" -t dmcss -dcr_ini "$dcr_ini" -p Css
	printf "\n"
	# 安装 ASM
	asm_type=$([ "$dm_install_mode" == "dsc" ] && echo "dmasmsvr" || echo "dmasmsvrm")
	run_cmd "$installer" -t "$asm_type" -dcr_ini "$dcr_ini" -p Asmsvr -y "$DM_CSS_SERVICE"
}
#========================================================
# 函数：dsc_start_ca
# 作用：循环启动所有节点的 CSS 与 ASM 服务
#========================================================
function dsc_start_ca() {
	log_print "启动 CSS 和 ASM 服务"
	local WAIT_TIME=5
	# 内部工具 - 跨节点启动服务
	_start_on_nodes() {
		local svc=$1 cmd=$2
		color_printf blue "启动 ${svc} 服务"
		printf "\n"
		local remote_script
		remote_script=$(
			declare -f run_cmd color_printf exec_ssh_cmd
			cat <<EOF
run_cmd su - dmdba -c '$cmd'
EOF
		)
		for ip in "${all_pubip_arr[@]}"; do
			while :; do
				color_printf yellow "节点 ${ip} 的 ${svc} 正在启动，请等待..."
				printf "\n"
				if exec_ssh_cmd "$ip" "$remote_script"; then
					break
				fi
				color_printf red "节点 ${ip} 的 ${svc} 启动失败，${WAIT_TIME}s 后重试..."
				sleep "$WAIT_TIME"
			done
			echo
		done
	}
	# 启动 CSS
	_start_on_nodes "CSS" "$CSS_START_CMD"
	# 启动 ASM
	_start_on_nodes "ASM" "$ASMSVR_START_CMD"
}
#========================================================
# 函数：create_disk_groups
# 作用：根据冗余级别生成 CREATE DISKGROUP SQL
#========================================================
function create_disk_groups() {
	local redundancy=$1 # 冗余级别：EXTERNAL / NORMAL / HIGH
	local group_name=$2 # 磁盘组名
	local disk_str=$3   # 逗号分隔的磁盘列表字符串
	# 1：将磁盘列表拆成数组
	IFS=',' read -r -a disks_arr <<<"$disk_str"
	local total=${#disks_arr[@]}
	# 2：初始化 CREATE DISKGROUP 语句
	local sql
	sql="CREATE DISKGROUP '$(upper "$group_name")' $redundancy REDUNDANCY"
	# 3：内部函数 - 生成 FAILGROUP 子句
	_build_failgroup_clause() {
		local begin=$1 end=$2 gname=$3 suffix=$4
		local paths=""
		for ((j = begin; j <= end; j++)); do
			paths+="'${ASM_DEVICE_PATH}/${gname}$(printf '%02d' $j)',"
		done
		paths="${paths%,}"
		sql+=" FAILGROUP '${gname}${suffix}' ASMDISK $paths"
	}
	# 4：根据冗余级别划分故障组
	case $redundancy in
	EXTERNAL)
		_build_failgroup_clause 1 "$total" "$group_name" "01"
		;;
	NORMAL)
		local mid=$((total / 2))
		_build_failgroup_clause 1 $mid "$group_name" "01"
		_build_failgroup_clause $((mid + 1)) "$total" "$group_name" "02"
		;;
	*)
		local t=$((total / 3))
		_build_failgroup_clause 1 $t "$group_name" "01"
		_build_failgroup_clause $((t + 1)) $((2 * t)) "$group_name" "02"
		_build_failgroup_clause $((2 * t + 1)) "$total" "$group_name" "03"
		;;
	esac
	# 5：追加属性
	sql+=" ATTRIBUTE AU_SIZE=${au_size}, REDO_SIZE=${redo_size}"
	# 6：追加到全局 SQL
	full_sql+="${sql}"$'\n'
}
#========================================================
# 函数：dsc_dmasmtool
# 作用：根据安装模式一次性创建或扩容所有 ASM 磁盘组
#========================================================
function dsc_dmasmtool() {
	log_print "创建 ASM 磁盘组"
	local tool disk_path
	# 1：选择工具
	tool=$([ "$dm_install_mode" = "dsc" ] && echo "dmasmtool" || echo "dmasmtoolm")
	# 2：构造命令前缀
	local dmasmtool_cmd="su - dmdba -c \"${env_dm_home}/bin/${tool} DCR_INI=${data_dir}/${dsc_db_name}/dmdcr.ini\""
	# 3：初始化 SQL 变量
	local full_sql=""
	# 4：冗余级别映射函数
	_mode_para_redun() {
		case $1 in
		1) echo "EXTERNAL" ;;
		2) echo "NORMAL" ;;
		*) echo "HIGH" ;;
		esac
	}
	# 5：计算起始索引
	local start_idx=0
	[[ "$dm_install_mode" = "dsc" ]] && start_idx=2 || start_idx=1
	# 6：建立映射
	declare -A asm_redun=(
		[dmlog]=$redo_redun
		[dmarch]=$arch_redun
		[dmdata]=$data_redun
	)
	# 7：遍历磁盘组生成 SQL
	for ((i = start_idx; i < ${#asm_disk_arr[@]}; i++)); do
		local dg_name="${asm_disk_arr[i]}"
		local disk_str="${disk_arr[i]}"
		if [ "$dm_install_mode" = "dsc" ]; then
			IFS=',' read -ra disks <<<"$disk_str"
			for ((k = 0; k < ${#disks[@]}; k++)); do
				disk_path="${ASM_DEVICE_PATH}/${dg_name}$(printf '%02d' $((k + 1)))"
				if ((k == 0)); then
					full_sql+="CREATE DISKGROUP '$(upper "$dg_name")' ASMDISK '${disk_path}'"$'\n'
				else
					# shellcheck disable=SC2089
					full_sql+="ALTER DISKGROUP '$(upper "$dg_name")' ADD ASMDISK '${disk_path}'"$'\n'
				fi
			done
		else
			create_disk_groups "$(_mode_para_redun "${asm_redun[$dg_name]}")" "$dg_name" "$disk_str"
		fi
	done
	# 循环结束后补上 exit
	full_sql+="exit"$'\n'
	# 定义内部重试函数
	_retry_dmasm_command() {
		local command="$1"
		local max_retries="$2" # 0 表示无限重试
		local silent="$3"      # 1 表示静默模式
		local retry_count=0
		_execute_command() {
			if [ "$silent" -eq 1 ]; then
				eval "$dmasmtool_cmd" <<<"$command" >/dev/null 2>&1
			else
				eval "$dmasmtool_cmd" <<<"$command"
			fi
		}
		while [ "$max_retries" -eq 0 ] || [ "$retry_count" -lt "$max_retries" ]; do
			((retry_count++))
			_execute_command && return 0
			sleep 5
		done
		return 1
	}
	# 8：判断 asm 实例是否启动（无限重试 + 静默模式）
	_retry_dmasm_command "exit" 0 1 || return 1 # 失败直接返回不显示提示
	# 9：统一执行 SQL（最多重试 10 次 + 非静默模式）
	local max_retry=10
	_retry_dmasm_command "$full_sql" "$max_retry" 0 || {
		printf "\n"
		color_printf red "ASM 磁盘组创建失败"
		printf "\n"
		return 1
	}
	printf "\n"
	# 9：保存最终 SQL
	sysinfo[dmasmtoolCmd]="$(escape_string "${full_sql}")"
}
#========================================================
# 函数：dsc_init_db
# 作用：生成 dminit.ini 并初始化数据库
#========================================================
function dsc_init_db() {
	log_print "初始化达梦数据库"
	color_printf blue "配置 dminit.ini"
	printf "\n"
	local dsc_dminit_file="$data_dir/$dsc_db_name/dminit.ini"
	local ep_name config1 config2 asm_last_path
	# 定义通用参数
	asm_last_path="+$(upper "${asm_disk_arr[@]: -1}")/dmdata"
	local -A paths_and_sizes=(
		[DB_NAME]="$dsc_db_name"                            # 数据库db_name名字
		[SYSTEM_PATH]="${asm_last_path}"                    # 系统文件路径
		[SYSTEM]="${asm_last_path}/$dsc_db_name/SYSTEM.DBF" # 系统文件路径和名称
		[SYSTEM_SIZE]="128"                                 # 系统文件大小
		[ROLL]="${asm_last_path}/$dsc_db_name/ROLL.DBF"     # 回滚文件路径和名称
		[ROLL_SIZE]="128"                                   # 回滚文件大小
		[MAIN]="${asm_last_path}/$dsc_db_name/MAIN.DBF"     # 主文件路径和名称
		[MAIN_SIZE]="128"                                   # 主文件大小
		[CTL_PATH]="${asm_last_path}/$dsc_db_name/dm.ctl"   # 控制文件路径和名称
		[LOG_SIZE]="$log_size"                              # 日志文件大小
		[DCR_SEQNO]="0"                                     # ASM磁盘序列号
		[AUTO_OVERWRITE]="2"                                # 自动覆盖策略
		[EXTENT_SIZE]="$extent_size"                        # 区大小
		[PAGE_SIZE]="$page_size"                            # 页大小
		[CASE_SENSITIVE]="$case_sensitive"                  # 是否大小写敏感
		[CHARSET]="$charset"                                # 字符集
		[BLANK_PAD_MODE]="$blank_pad_mode"                  # 空格填充模式（打开兼容Oracle参数时候使用）
		[CTL_MIRROR]="$ctl_mirror"                          # dm.ctl 和 dm_service.prikey 文件副本数
		[DATA_MIRROR]="$data_mirror"                        # DATA_MIRROR: 数据镜副本数
		[LOG_MIRROR]="$log_mirror"                          # LOG_MIRROR: 日志镜像副本数
		[DATA_STRIPING]="$data_striping"                    # DATA_STRIPING: 数据条带化粒度
		[LOG_STRIPING]="$log_striping"                      # LOG_STRIPING: 日志条带化粒度
		[SYSDBA_PWD]="'${sysdba_pwd_temp}'"                 # 数据库密码
		[SYSAUDITOR_PWD]="'${sysauditor_pwd_temp}'"         # 数据库审计员密码
	)
	[[ "$db_flag" == "true" ]] && paths_and_sizes[SYSSSO_PWD]="'${syssso_pwd_temp}'"
	[[ $length_in_char =~ [Yy1] ]] && paths_and_sizes[LENGTH_IN_CHAR]="$length_in_char"
	if [[ -n "$dminit_para_value" ]]; then
		dminit_para_value=$(echo "$dminit_para_value" | awk '{gsub(/ *= */, "="); print}')
		for pair in $dminit_para_value; do
			key="${pair%%=*}"
			value="${pair#*=}"
			paths_and_sizes["$key"]="$value"
		done
	fi
	# 生成 dminit.ini 通用部分
	config1=$(
		cat <<EOF
$(for key in "${!paths_and_sizes[@]}"; do
			if [[ $dm_install_mode == "dsc" && $key =~ ^(CTL_MIRROR|DATA_MIRROR|LOG_MIRROR|DATA_STRIPING|LOG_STRIPING)$ ]]; then
				continue
			fi
			echo "$key = ${paths_and_sizes[$key]}"
		done)
EOF
	)
	if [[ $dm_install_mode == "dsc" ]]; then
		config1+=$(
			cat <<EOF

DCR_PATH=$ASM_DEVICE_PATH/${asm_disk_arr[0]}01
EOF
		)
	else
		config1+=$(
			cat <<EOF

DCR_PATH=$ASM_DEVICE_PATH
EOF
		)
	fi
	# 生成各节点专属部分
	a=$([[ $dm_install_mode == "dsc" ]] && echo "2" || echo "1")
	for ((x = 0; x < ${#malip_host_arr[@]}; x++)); do
		ep_name="${dsc_ep_name}$(printf "%02d" $((x + 1)))"
		if [[ -z "$log_disk" ]]; then
			log_path="+$(upper "${asm_disk_arr[@]: -1}")/dmdata/$dsc_db_name/$ep_name"
		else
			log_path="+$(upper "${asm_disk_arr[$a]}")/dmlog/$dsc_db_name/$ep_name"
		fi
		config2+=$(
			cat <<EOF

[$(upper "$ep_name")]
CONFIG_PATH = $data_dir/$dsc_db_name/${ep_name}_config
PORT_NUM = $port_num
MAL_HOST = ${malip_host_arr[$x]}
MAL_PORT = ${db_mal_port}
LOG_PATH = ${log_path}_log01.log
LOG_PATH = ${log_path}_log02.log
EOF
		)
	done

	# 写入 dminit.ini 并执行初始化
	run_cmd su - dmdba <<EOF
printf "%s\n%s\n" "$config1" "$(echo "$config2" | sed '1{/^$/d}') " | tee -a "$dsc_dminit_file"
EOF
	printf "\n"
	color_printf blue "初始化数据库"
	printf "\n"
	run_cmd su - dmdba -c "$env_dm_home/bin/dminit control=$dsc_dminit_file"
	printf '\n\033[34m%s\033[0m\n' '数据库初始化完成'
	# 添加配置文件到关联数组
	sysinfo[dscDminitFile]="$(escape_string "$(cat "$dsc_dminit_file")")"
}
#========================================================
# 函数：dsc_startup
# 作用：注册并启动所有节点的数据库服务
#========================================================
function dsc_startup() {
	log_print "注册服务并启动数据库"
	local ep_cnt=${#all_pubip_arr[@]}
	local cfg_root="$data_dir/$dsc_db_name"
	# 工具函数 - 同步配置
	_sync_cfg() {
		local ip=$1 ep=$2
		exec_scp_cmd "$cfg_root/${ep}_config" "$ip" "$cfg_root" "UP"
		exec_ssh_cmd "$ip" "chown -R dmdba:dinstall $cfg_root/${ep}_config"
	}
	# 工具函数 - 统一修改 dmdcr.ini
	_fix_dmdcr() {
		local ip=$1
		exec_ssh_cmd "$ip" "sed -i '4s/0/180/;6s/0/360/' $cfg_root/dmdcr.ini"
	}
	# 工具函数 - 注册服务
	_reg_svc() {
		local ip=$1 ep=$2
		exec_ssh_cmd "$ip" "$env_dm_home/script/root/dm_service_installer.sh -t dmserver -dm_ini $cfg_root/${ep}_config/dm.ini -dcr_ini $cfg_root/dmdcr.ini -p ${dsc_db_name} -y $DM_ASM_SERVICE"
	}
	# 工具函数 - 启动数据库
	_start_db() {
		local ip=$1
		exec_ssh_cmd "$ip" "until su - dmdba -c '$DB_START_CMD'; do sleep 5; done"
		color_printf green "节点 $ip 启动成功"
		echo
	}
	# 主循环
	for ((i = 0; i < ep_cnt; i++)); do
		local ip=${all_pubip_arr[$i]}
		# shellcheck disable=SC2155
		local ep=${dsc_ep_name}$(printf "%02d" $((i + 1)))
		color_printf blue "在节点 $ip 注册服务并启动数据库"
		echo
		((i > 0)) && _sync_cfg "$ip" "$ep"
		_fix_dmdcr "$ip"
		_reg_svc "$ip" "$ep"
		printf "\n"
		_start_db "$ip"
	done
}
#========================================================
# 函数：query_basic_para
# 作用：查询并生成数据库基础参数信息脚本
#========================================================
function query_basic_para() {
	# 1：生成查询数据库基本信息的 SQL 脚本
	log_print "创建查询数据库基础参数信息脚本"
	local basic_dbinfo="${scripts_dir}/basic_dbinfo.sql"
	local sql_text
	sql_text=$(
		cat <<'EOF'
set echo off
set timing off
set feedback off
set lineshow off
WITH INSTANCE_INFO AS
     ( SELECT '数据库版本' AS 数据库参数项, BUILD_VERSION AS 数据库参数值 FROM V\$INSTANCE
     UNION ALL
     SELECT 'PACK版本号', ID_CODE()
     UNION ALL
     SELECT '数据库名', NAME FROM V\$DATABASE
     UNION ALL
     SELECT '实例名', INSTANCE_NAME FROM V\$INSTANCE
     )
     ,
     SYSTEM_INFO AS
     ( SELECT '簇大小', CAST(SF_GET_EXTENT_SIZE() AS VARCHAR)
     UNION ALL
     SELECT '页大小', CAST(PAGE() / 1024 AS VARCHAR)
     UNION ALL
     SELECT '大小写敏感',
            CAST(DECODE(SF_GET_CASE_SENSITIVE_FLAG(),
                               0, '不敏感',
                               1, '敏感') AS VARCHAR)
     UNION ALL
     SELECT '字符集',
            CAST(DECODE(SF_GET_UNICODE_FLAG(),
                               0, 'GB18030',
                               1, 'UTF-8',
                               2, 'EUC-KR') AS VARCHAR)
     UNION ALL
     SELECT '兼容其他数据库模式',
            CAST(
            CASE (SELECT PARA_VALUE
                     FROM V\$DM_INI
                    WHERE PARA_NAME = 'COMPATIBLE_MODE')
                   WHEN '0'
                   THEN '不兼容'
                   WHEN '1'
                   THEN '兼容 SQL92 标准'
                   WHEN '2'
                   THEN '部分兼容 ORACLE（需要与 FLOAT_MODE=1 同时开启）'
                   WHEN '3'
                   THEN '部分兼容 MS SQL SERVER'
                   WHEN '4'
                   THEN '部分兼容 MYSQL'
                   WHEN '5'
                   THEN '兼容 DM6'
                   WHEN '6'
                   THEN '部分兼容 TERADATA'
                   WHEN '7'
                   THEN '部分兼容 POSTGRES'
                   WHEN '8'
                   THEN '部分兼容 DB2'
                   ELSE '未知模式（参数值：' || (SELECT PARA_VALUE
                             FROM V\$DM_INI
                            WHERE PARA_NAME = 'COMPATIBLE_MODE') || '）'
            END AS VARCHAR(100) )
     UNION ALL
     SELECT '结尾空格填充模式是否兼容ORACLE',
            CAST(DECODE( (SELECT PARA_VALUE
                                     FROM V\$DM_INI
                                    WHERE PARA_NAME = 'BLANK_PAD_MODE'),
                               0, '不兼容',
                               1, '兼容' ) AS VARCHAR)
     )
     ,
     LICENSE_INFO AS
     ( SELECT '授权码', SERIES_NO FROM V\$LICENSE
     UNION ALL
     SELECT '集群类型', CLUSTER_TYPE FROM V\$LICENSE
     )
SELECT * FROM INSTANCE_INFO
UNION ALL
SELECT * FROM SYSTEM_INFO
UNION ALL
SELECT * FROM LICENSE_INFO;
exit;
EOF
	)
	# 2：将 SQL 脚本写入本地文件
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${basic_dbinfo}" >/dev/null
EOF
	# 3：列出生成的脚本文件
	run_cmd ls -lh "${basic_dbinfo}"
}
#========================================================
# 函数：si_dmarch
# 作用：为单机实例生成归档配置脚本
#========================================================
function si_dmarch() {
	# 1：创建归档目录并赋权
	local db_arch_dir="$arch_dir/$db_name"
	run_cmd mkdir -p -m 755 "$arch_dir" "$db_arch_dir"
	run_cmd chown -R dmdba:dinstall "$arch_dir" "$db_arch_dir"
	# 2：生成归档配置 SQL 脚本（改为变量累加方式）
	local si_dmarch_file="${scripts_dir}/conf_arch.sql"
	local sql_text
	sql_text=$(
		cat <<EOF
set echo off timing off feedback off lineshow off
ALTER DATABASE MOUNT;
ALTER DATABASE ARCHIVELOG;
ALTER DATABASE ADD ARCHIVELOG 'DEST=$db_arch_dir, TYPE=LOCAL, FILE_SIZE=$log_size, SPACE_LIMIT=$space_limit';
ALTER DATABASE OPEN;
select arch_mode from v\\\$database;
SELECT arch_name, arch_dest, arch_file_size, arch_space_limit FROM v\\\$dm_arch_ini;
exit;
EOF
	)
	# 3：写入文件
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${si_dmarch_file}" >/dev/null
EOF
}
#========================================================
# 函数：dsc_dmarch
# 作用：为 DSC/DSCM 集群生成归档配置脚本
#========================================================
function dsc_dmarch() {
	local dsc_dmarch_file="${scripts_dir}/conf_arch.sql"
	local sql_text="" # 用于累加 SQL 语句
	# 初始化 SQL
	sql_text+=$(
		cat <<EOF
ALTER DATABASE MOUNT;
ALTER DATABASE ARCHIVELOG;
EOF
	)
	# 内部工具函数：添加归档目的地
	_arch_log_dest() {
		local dest_type=$1
		local dest_path=$2
		local file_size=$3
		local space_limit_value=$4
		local incoming_path=$5
		sql_text+=$(
			cat <<EOF
	
ALTER DATABASE ADD ARCHIVELOG 'DEST = $dest_path, TYPE = $dest_type, FILE_SIZE = $file_size, SPACE_LIMIT = $space_limit_value ${incoming_path:+, INCOMING_PATH = $incoming_path}';
EOF
		)
	}
	_loc_arch_dest() {
		_arch_log_dest "local" "$1" "$log_size" "$space_limit_value"
	}
	_rem_arch_dest() {
		local asm_path=$1 remote_dest_path incoming_path
		for ((x = 1; x <= ${#all_pubip_arr[@]}; x++)); do
			[[ $x -ne $node_num ]] || continue
			remote_dest_path="$(upper "${dsc_ep_name}")$(printf "%02d" "$x")"
			incoming_path="$asm_path/$dsc_db_name/${dsc_ep_name}$(printf "%02d" "$x")"
			_arch_log_dest "REMOTE" "$remote_dest_path" "$log_size" "$space_limit_value" "$incoming_path"
		done
	}
	# 计算 space_limit_value
	if [[ -z $arch_disk && $is_local_arch = "N" ]]; then
		space_limit_value=$space_limit
	else
		space_limit_value=$(awk "BEGIN { printf \"%.0f\", ($space_limit - 10240) / ${#all_pubip_arr[@]} }")
	fi
	# 根据参数组合添加归档目的地
	if [[ -z $arch_disk ]]; then
		# 未使用 ASM
		if [[ $is_local_arch = "N" ]]; then
			asm_path="+$(upper "${asm_disk_arr[@]: -1}")/dmarch"
			_loc_arch_dest "$asm_path/$dsc_db_name/${dsc_ep_name}$(printf "%02d" "$node_num")"
			_rem_arch_dest "$asm_path"
		else
			_loc_arch_dest "$arch_dir/$dsc_db_name/${dsc_ep_name}$(printf "%02d" "$node_num")"
			_rem_arch_dest "$arch_dir/$dsc_db_name"
		fi
	else
		# 使用 ASM
		asm_path="+$(upper "${asm_disk_arr[@]: -2}")/dmarch"
		if [[ $is_local_arch = "N" ]]; then
			_loc_arch_dest "$asm_path/$dsc_db_name/${dsc_ep_name}$(printf "%02d" "$node_num")"
			_rem_arch_dest "$asm_path"
		else
			_loc_arch_dest "$asm_path/$dsc_db_name/${dsc_ep_name}$(printf "%02d" "$node_num")"
			_rem_arch_dest "$asm_path"
			_loc_arch_dest "$arch_dir/$dsc_db_name/${dsc_ep_name}$(printf "%02d" "$node_num")"
		fi
	fi
	# 追加 OPEN & 退出
	sql_text+=$(
		cat <<EOF

ALTER DATABASE OPEN;
exit;
EOF
	)
	# 将 SQL 脚本写入本地文件
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${dsc_dmarch_file}" >/dev/null
EOF
}
#========================================================
# 函数：db_bak
# 作用：生成全量/增量/归档备份及定时作业脚本
#========================================================
function db_bak() {
	# 1：提取参数并创建备份目录
	local name="$1" bak_path
	bak_path="$backup_dir/$name/bak"
	run_cmd mkdir -m 755 -p "$bak_path"
	run_cmd chown -R dmdba:dinstall "$bak_path"
	# 2：根据备份模式生成全量/增量/归档 SQL
	if [[ $backup_mode -eq 1 ]]; then
		# 2.1：仅全量备份
		config_full=$(
			cat <<EOF
call SP_CREATE_JOB('bak_full',1,0,'',0,0,'',0,'每天凌晨01:05全量备份，并删除30天之前的备份。');
call SP_JOB_CONFIG_START('bak_full');
call SP_JOB_SET_EP_SEQNO('bak_full', 0);
call SP_ADD_JOB_STEP('bak_full', 'bak_full', 6, '01000000$bak_path', 3, 1, 0, 0, NULL, 0);
call SP_ADD_JOB_STEP('bak_full', 'bak_del', 0, 'CALL SF_BAKSET_BACKUP_DIR_ADD(''DISK'',''$bak_path'');
CALL SP_DB_BAKSET_REMOVE_BATCH(''DISK'',NOW()-30);', 1, 1, 0, 0, NULL, 0);
call SP_ADD_JOB_SCHEDULE('bak_full', 'bak_full', 1, 1, 1, 0, 0, '01:05:00', NULL, '2020-01-01 00:00:00', NULL, '');
call SP_JOB_CONFIG_COMMIT('bak_full');
EOF
		)
		sysinfo["config_full"]="$config_full"
		sysinfo["config_inc"]="每天全备不做增量备份"
		# 2.2：归档备份
		config_arch=$(
			cat <<EOF
call SP_CREATE_JOB('bak_arch',1,0,'',0,0,'',0,'每天凌晨03:05备份归档，删除30天之前的备份');
call SP_JOB_CONFIG_START('bak_arch');
call SP_JOB_SET_EP_SEQNO('bak_arch', 0);
call SP_ADD_JOB_STEP('bak_arch', 'bak_arch', 6, '30000000$bak_path', 3, 0, 0, 0, NULL, 0);
call SP_ADD_JOB_STEP('bak_arch', 'bak_del', 0, 'CALL SF_BAKSET_BACKUP_DIR_ADD(''DISK'',''$bak_path'');
CALL SP_ARCH_BAKSET_REMOVE_BATCH(''DISK'',NOW()-30);', 0, 0, 0, 0, NULL, 0);
call SP_ADD_JOB_SCHEDULE('bak_arch', 'back_arch', 1, 1, 1, 0, 0, '03:05:00', NULL, '2023-05-24 18:16:05', NULL, '');
call SP_JOB_CONFIG_COMMIT('bak_arch');
call SP_ENABLE_JOB('bak_arch', 0);
EOF
		)
		sysinfo["config_arch"]="$config_arch"
		# 2.3：拼接完整 SQL
		config_fullbackup=$(
			cat <<EOF
set echo off
set timing off
set feedback off
set lineshow off
SP_INIT_JOB_SYS(1);
${sysinfo[config_full]}
${sysinfo[config_arch]}
SELECT NAME, describe FROM sysjob.sysjobs;
SELECT top 1 NAME, command FROM "SYSJOB"."SYSJOBSTEPS";
exit;
EOF
		)
		run_cmd su - dmdba <<EOF
printf "%s\n" "$config_fullbackup" | tee "${scripts_dir}/conf_fullbackup.sql" >/dev/null
EOF
		run_cmd ls -lh "${scripts_dir}/conf_fullbackup.sql"
	else
		# 3：全量+增量+归档备份
		config_full=$(
			cat <<EOF
call SP_CREATE_JOB('bak_full',1,0,'',0,0,'',0,'周六凌晨01:05做全量备份，并删除30天之前的备份。');
call SP_JOB_CONFIG_START('bak_full');
call SP_JOB_SET_EP_SEQNO('bak_full', 0);
call SP_ADD_JOB_STEP('bak_full', 'bak_full', 6, '01000000$bak_path', 3, 1, 0, 0, NULL, 0);
call SP_ADD_JOB_STEP('bak_full', 'bak_del', 0, 'CALL SF_BAKSET_BACKUP_DIR_ADD(''DISK'',''$bak_path'');
CALL SP_DB_BAKSET_REMOVE_BATCH(''DISK'',NOW()-30);', 1, 1, 0, 0, NULL, 0);
call SP_ADD_JOB_SCHEDULE('bak_full', 'bak_full', 1, 2, 1, 64, 0, '01:05:00', NULL, '2020-01-01 00:00:00', NULL, '');
call SP_JOB_CONFIG_COMMIT('bak_full');
EOF
		)
		sysinfo["config_full"]="$config_full"
		config_inc=$(
			cat <<EOF
call SP_CREATE_JOB('bak_inc',1,0,'',0,0,'',0,'周日到周五凌晨01:05做增量备份,如果失败,做全量备份');
call SP_JOB_CONFIG_START('bak_inc');
call SP_ADD_JOB_STEP('bak_inc', 'bak_inc', 6, '11000000$bak_path|$bak_path', 1, 3, 2, 6, NULL, 0);
call SP_ADD_JOB_STEP('bak_inc', 'switch_bak_full', 6, '01000000$bak_path', 1, 1, 0, 0, NULL, 0);
call SP_ADD_JOB_SCHEDULE('bak_inc', 'bak_inc', 1, 2, 1, 63, 0, '01:05:00', NULL, '2020-01-01 00:00:00', NULL, '');
call SP_JOB_CONFIG_COMMIT('bak_inc');
EOF
		)
		sysinfo["config_inc"]="$config_inc"
		config_arch=$(
			cat <<EOF
call SP_CREATE_JOB('bak_arch',1,0,'',0,0,'',0,'每天凌晨03:05归档备份，删除30天之前的备份');
call SP_JOB_CONFIG_START('bak_arch');
call SP_JOB_SET_EP_SEQNO('bak_arch', 0);
call SP_ADD_JOB_STEP('bak_arch', 'bak_arch', 6, '30000000$bak_path', 3, 0, 0, 0, NULL, 0);
call SP_ADD_JOB_STEP('bak_arch', 'bak_del', 0, 'CALL SF_BAKSET_BACKUP_DIR_ADD(''DISK'',''$bak_path'');
CALL SP_ARCH_BAKSET_REMOVE_BATCH(''DISK'',NOW()-30);', 0, 0, 0, 0, NULL, 0);
call SP_ADD_JOB_SCHEDULE('bak_arch', 'back_arch', 1, 1, 1, 0, 0, '03:05:00', NULL, '2023-05-24 18:16:05', NULL, '');
call SP_JOB_CONFIG_COMMIT('bak_arch');
call SP_ENABLE_JOB('bak_arch', 0);
EOF
		)
		sysinfo["config_arch"]="$config_arch"
		# 4：拼接完整 SQL
		config_incrbackup=$(
			cat <<EOF
set echo off
set timing off
set feedback off
set lineshow off
SP_INIT_JOB_SYS(1);
${sysinfo[config_full]}
${sysinfo[config_inc]}
${sysinfo[config_arch]}
SELECT NAME, describe FROM sysjob.sysjobs;
SELECT top 1 NAME, command FROM "SYSJOB"."SYSJOBSTEPS";
exit;
EOF
		)
		run_cmd su - dmdba <<EOF
printf "%s\n" "$config_incrbackup" | tee "${scripts_dir}/conf_incrbackup.sql" >/dev/null
EOF
		run_cmd ls -lh "${scripts_dir}/conf_incrbackup.sql"
	fi
	# 5：生成检查备份 SQL
	sql_text=$(
		cat <<EOF
DECLARE
    v_job_id sysjob.sysjobs.id%TYPE;
BEGIN
    -- 获取 bak_full 作业 ID
    BEGIN
        SELECT id INTO v_job_id
        FROM sysjob.sysjobs
        WHERE upper(name) = 'BAK_FULL';
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            DBMS_OUTPUT.PUT_LINE('没有找到名称为 BAK_FULL 的作业。');
            RETURN;
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('发生错误: ' || SQLERRM);
            RETURN;
    END;
    -- 执行 bak_full 作业
    BEGIN
        EXECUTE IMMEDIATE 'call sp_dbms_job_run(' || v_job_id || ')';
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('运行作业时发生错误: ' || SQLERRM);
            RETURN;
    END;
    -- 添加备份路径到数据库
    BEGIN
        EXECUTE IMMEDIATE 'call sf_bakset_backup_dir_add(''DISK'', ''$bak_path'')';
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('调用存储过程 sf_bakset_backup_dir_add 时发生错误: ' || SQLERRM);
            RETURN;
    END;
    -- 查询备份集
    FOR hist_rec IN (SELECT DEVICE_TYPE, BACKUP_PATH, TYPE, RANGE# FROM V\\\$BACKUPSET) LOOP
        DBMS_OUTPUT.PUT_LINE('备份集: ' || hist_rec.BACKUP_PATH || '.');
    END LOOP;
END;
/
exit;
EOF
	)

	# 6：写入检查备份脚本
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${scripts_dir}/check_backup.sql" >/dev/null
EOF
}
#========================================================
# 函数：create_baseline_sql
# 作用：统一创建归档与备份脚本
#========================================================
function create_baseline_sql() {
	# 1：打印提示
	log_print "创建归档，备份脚本"
	# 2：创建归档脚本
	color_printf blue "创建数据库归档脚本："
	printf "\n"
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
		dsc_dmarch
	else
		si_dmarch
	fi
	run_cmd ls -lh "${scripts_dir}/conf_arch.sql"
	printf "\n"
	# 3：创建备份脚本
	color_printf blue "创建数据库备份脚本："
	printf "\n"
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
		db_bak "$dsc_db_name"
	else
		db_bak "$db_name"
	fi
}
#========================================================
#函数：create_optimize_sql
#作用：生成达梦数据库优化相关 SQL 脚本
#========================================================
function create_optimize_sql() {
	# 1：打印开始提示
	log_print "创建达梦数据库优化脚本"
	# 2：定义脚本文件路径
	local conf_dmini="${scripts_dir}/conf_dmini.sql"
	local conf_dmini_other="${scripts_dir}/conf_dmini_other.sql"
	local query_script="${scripts_dir}/query_dmini.sql"
	local conf_stat_script="${scripts_dir}/conf_statistics.sql"
	local conf_com_mode_script="${scripts_dir}/conf_com_mod.sql"
	# 3：创建数据库参数配置脚本
	color_printf blue "创建数据库参数配置脚本:"
	printf "\n"
	# 4：判断是否为 DSC 集群
	local is_dsc=0
	[[ $dm_install_mode =~ ^(dsc|dscm)$ ]] && is_dsc=1
	# 5：生成 conf_dmini.sql 内容
	sql_text=$(
		cat <<EOF
--本脚本为安全生产环境设置参数作为参考，并不追求极致性能
SET ECHO OFF
SET TIMING OFF
SET FEEDBACK OFF
SET LINESHOW OFF
SET SERVEROUTPUT ON
declare
    exec_mode            int := 0;            --0表示脚本自动获取机器的内存和CPU配置直接执行脚本修改参数；1表示不直接修改参数，只打印设置参数的语句，设置为1后，必须调整v_mem_mb和v_cpus
    is_dsc               int := $is_dsc;      --是否是dsc集群,如果是dsc集群请设置为1，将自动调整dsc相关参数
    mem_per              int := $mem_percent; --默认机器80%内存归达梦数据库使用，可根据实际需求调整此参数; MAX_OS_MEMORY强制100不与此参数挂钩
    v_mem_mb             int := 64000;        --exec_mode为1时请自行根据机器实际内存调整此参数，单位为M；v_mem_mb*(mem_per/100)小于4G,将不做调整
    v_cpus               int := 64;           --exec_mode为1时请自行根据机器实际CPU核数调整此参数
    oltp_mode            int := $oltp_mode;   --并发量较高的OLTP类型系统此参数设置为1，并发量不高的一般业务系统和OLAP类的系统此参数设置为0,影响SORT_FLAG和UNDO_RETENTION
    pk_cluster_mode      int := 0;            --是否使用聚集主键：性能要求高且大字段较少的业务场景强烈建议设置为1，大字段多的场景设置为0
    ini_bak              int := 1;            --是否建一个表备份老的dm.ini,1为保存，0为不保存，默认不保存
    tname                varchar(100);
    MAX_SESSIONS         INT := 100; --本脚本根据内存参数自动计算能支持的最大连接数MAX_SESSIONS，如计算出的MAX_SESSIONS不满足生产要求，建议给机器加内存资源
    MEMORY_POOL          int;
    MEMORY_N_POOLS       int;
    MEMORY_TARGET        int;
    BUFFER               INT;
    MAX_BUFFER           INT;
    RECYCLE              int;
    CACHE_POOL_SIZE      int;
    BUFFER_POOLS         int;
    RECYCLE_POOLS        int;
    SORT_BUF_SIZE        int;
    SORT_BUF_GLOBAL_SIZE INT;
    DICT_BUF_SIZE        INT := 50;
    HJ_BUF_SIZE          INT;
    HAGR_BUF_SIZE        INT;
    HJ_BUF_GLOBAL_SIZE   INT;
    HAGR_BUF_GLOBAL_SIZE INT;
    SORT_FLAG            INT;
    SORT_BLK_SIZE        INT;
    RLOG_POOL_SIZE       INT;
    TASK_THREADS         INT;
    IO_THR_GROUPS        INT;
    FAST_POOL_PAGES      INT := 3000;
    FAST_ROLL_PAGES      INT := 1000;
    UNDO_RETENTION       INT := 90;
    VM_POOL_TARGET       INT := 8192;
    SESS_POOL_TARGET     INT := 8192;
    CNT                  INT;
    VER0                 INT := 0;
    VER1                 INT := 0;
    VER2                 INT := 0;
    flag                 INT := 0;
begin
    CNT := 0;
    if exec_mode = 0 then
        SELECT TOP 1                   N_CPU,
               TOTAL_PHY_SIZE / 1024 / 1024
          INTO v_cpus,
               v_mem_mb
          FROM V\$SYSTEMINFO;
    end if;
    if v_mem_mb >= 128000 then
        flag := 1;
    end if;
    v_mem_mb := v_mem_mb * (mem_per / 100.0);
    v_mem_mb = round(v_mem_mb, -3);
    --内存4G以下的不做调整，采用默认参数
    IF v_mem_mb < 4000 THEN
        goto return_2000;
    END IF;
    --MEMORY_TARGET=round(cast(v_mem_mb * 0.10 as int),-3);
    MEMORY_TARGET = GREAT(FLOOR(cast(v_mem_mb * 0.10 as int) / 1000) * 1000, 500);
    TASK_THREADS  := 4;
    IO_THR_GROUPS := 8;
    IF v_cpus > 64 THEN
        v_cpus        := 64;
        TASK_THREADS  := 16;
        IO_THR_GROUPS := 32;
    ELSIF v_cpus > 8 THEN
        IO_THR_GROUPS := 16;
        TASK_THREADS  := 8;
    ELSE
        TASK_THREADS  := 4;
        IO_THR_GROUPS := 8;
    END IF;
    --BUFFER := round(cast(v_mem_mb * 0.4 as int),-3);
    BUFFER  := FLOOR(cast(v_mem_mb * 0.4 as int) / 1000) * 1000;
    RECYCLE := cast(v_mem_mb * 0.04 as int);
    IF v_mem_mb < 70000 THEN
        with t as
             ( select rownum rn from dual connect by level <= 100
             )
             ,
             t1 as
             ( select * from t where rn > 1
             minus
             select ta.rn * tb.rn
               from t       ta,
                    t       tb
              where ta.rn <= tb.rn
                and ta.rn > 1
                and tb.rn > 1
             )
          select top 1 rn
            into BUFFER_POOLS
            from t1
           where rn > v_mem_mb / 800
        order by 1;
    
    ELSE
        BUFFER_POOLS := 101;
    END IF;
    --修改内存池
    IF v_mem_mb    >= 16000 THEN
        IF v_mem_mb = 16000 THEN
            MEMORY_POOL          := 1000;
            SORT_BUF_GLOBAL_SIZE := 1000;
            MEMORY_N_POOLS       := 3;
            CACHE_POOL_SIZE      := 512;
            DICT_BUF_SIZE        := 256;
        ELSE
            MEMORY_POOL          := 2000;
            SORT_BUF_GLOBAL_SIZE := 2000;
            MEMORY_N_POOLS       := 11;
            CACHE_POOL_SIZE      := 1024;
            DICT_BUF_SIZE        := 512;
        END IF;
        FAST_POOL_PAGES := 9999;
        SORT_FLAG     = 0;
        SORT_BLK_SIZE = 1;
        SORT_BUF_SIZE        := 10;
        RLOG_POOL_SIZE       := 1024;
        HJ_BUF_GLOBAL_SIZE   := cast(v_mem_mb * 0.0625 as int);
        HAGR_BUF_GLOBAL_SIZE := cast(v_mem_mb * 0.0625 as int);
        HJ_BUF_SIZE          := 250;
        HAGR_BUF_SIZE        := 250;
        IF v_mem_mb >= 64000 THEN
            VM_POOL_TARGET   := 8192;
            SESS_POOL_TARGET := 8192;
            FAST_POOL_PAGES  := 99999;
            FAST_ROLL_PAGES  := 9999;
            BUFFER           := BUFFER - 3000;
            CACHE_POOL_SIZE  := 2048;
            RLOG_POOL_SIZE   := 2048;
            DICT_BUF_SIZE    := 1024;
            SORT_FLAG            = 0;
            SORT_BLK_SIZE        = 1;
            SORT_BUF_SIZE        = 20;
            SORT_BUF_GLOBAL_SIZE = cast(v_mem_mb * 0.04 as    int);
            HJ_BUF_GLOBAL_SIZE   := cast(v_mem_mb * 0.1 as    int);
            HAGR_BUF_GLOBAL_SIZE := cast(v_mem_mb * 0.0625 as int);
            HJ_BUF_SIZE          := 512;
            HAGR_BUF_SIZE        := 512;
            MEMORY_N_POOLS       := 59;
            IF v_mem_mb             >= 128000 OR flag = 1 THEN
                SORT_FLAG            = 1;
                SORT_BLK_SIZE        = 1;
                SORT_BUF_SIZE        = 50;
                SORT_BUF_GLOBAL_SIZE = cast(v_mem_mb * 0.1 as int);
            END IF;
            IF v_mem_mb             >= 256000 THEN
                SORT_FLAG            = 1;
                SORT_BLK_SIZE        = 2;
                SORT_BUF_SIZE        = 50;
                SORT_BUF_GLOBAL_SIZE = cast(v_mem_mb * 0.1 as int);
            END IF;
        END IF;
        HJ_BUF_GLOBAL_SIZE   := round(HJ_BUF_GLOBAL_SIZE, -3);
        HAGR_BUF_GLOBAL_SIZE := round(HAGR_BUF_GLOBAL_SIZE, -3);
        SORT_BUF_GLOBAL_SIZE := round(SORT_BUF_GLOBAL_SIZE, -3);
        RECYCLE              := round(RECYCLE, -3);
    ELSE
        MEMORY_POOL          := GREAT(cast(v_mem_mb * 0.0625 as int), 100);
        MEMORY_POOL          := round(MEMORY_POOL, -2);
        MEMORY_N_POOLS       := 2;
        CACHE_POOL_SIZE      := 200;
        RLOG_POOL_SIZE       := 256;
        SORT_BUF_SIZE        := 10;
        SORT_BUF_GLOBAL_SIZE := 500;
        DICT_BUF_SIZE        := 128;
        SORT_FLAG     = 0;
        SORT_BLK_SIZE = 1;
        HJ_BUF_GLOBAL_SIZE   := round(GREAT(cast(v_mem_mb * 0.0625 as  int), 500), -2);
        HAGR_BUF_GLOBAL_SIZE := round(GREAT(cast(v_mem_mb * 0.0625 as  int), 500), -2);
        HJ_BUF_SIZE          := round(GREAT(cast(v_mem_mb * 0.00625 as int), 50), -2);
        HAGR_BUF_SIZE        := round(GREAT(cast(v_mem_mb * 0.00625 as int), 50), -2);
        MAX_SESSIONS         := 100;
        VM_POOL_TARGET       := 8192;
        SESS_POOL_TARGET     := 8192;
    END IF;
    --设置根据RECYCLE情况RECYCLE_POOLS参数
    with t as
         ( select rownum rn from dual connect by level <= 100
         )
         ,
         t1 as
         ( select * from t where rn > 1
         minus
         select ta.rn * tb.rn
           from t       ta,
                t       tb
          where ta.rn <= tb.rn
            and ta.rn > 1
            and tb.rn > 1
         )
      select top 1 rn
        into RECYCLE_POOLS
        from t1
       where rn <= great(RECYCLE * 1024 / 3000 / (page() / 1024), 2)
    order by 1 desc;
    
    tname := 'BAK_DMINI_' || to_char(sysdate, 'yymmdd');
    execute IMMEDIATE 'select count(*) from USER_ALL_TABLES where table_name= ?' into CNT using tname;
    if exists(select 1 from V\$INSTANCE where MODE$ in ('NORMAL','PRIMARY')) then
        IF CNT=0 and ini_bak=1 THEN
            execute IMMEDIATE 'CREATE TABLE BAK_DMINI_' || to_char(sysdate,'yymmdd') || ' as select *,sysdate uptime from v\$dm_ini';
        ELSE
            IF CNT=1 THEN
                execute IMMEDIATE 'INSERT INTO  BAK_DMINI_' || to_char(sysdate,'yymmdd') || ' select *,sysdate uptime from v\$dm_ini';
                COMMIT;
            END IF;
        END IF;
    end if;
    --如果oltp_mode设置为1，采用旧的排序模式,undo_relation采用默认值
    if oltp_mode  = 1 then
        SORT_FLAG = 0;
        SORT_BUF_SIZE := 2;
    end if;
    --如果oltp_mode设置为0，undo_relation适当放大,采用新的排序方法
    if oltp_mode       = 0 then
        UNDO_RETENTION = 900;
    end if;
    MAX_BUFFER := BUFFER;
    SELECT TO_NUMBER(SUBSTR(VER, 1, 2), 'XX'),
           TO_NUMBER(SUBSTR(VER, 5, 2), 'XX'),
           TO_NUMBER(SUBSTR(VER, 7, 2), 'XX')
      into VER0,
           VER1,
           VER2
      FROM (SELECT RAWTOHEX(CAST(SUBSTR(VER, 3) AS INT)) AS VER
              FROM (SELECT REGEXP_SUBSTR(ID_CODE, '[^-]+', 1, 1) AS VER));
    
    select round((v_mem_mb - (MEMORY_TARGET + BUFFER + RECYCLE + HJ_BUF_GLOBAL_SIZE + HAGR_BUF_GLOBAL_SIZE + CACHE_POOL_SIZE + DICT_BUF_SIZE + SORT_BUF_GLOBAL_SIZE + RLOG_POOL_SIZE)) / ((VM_POOL_TARGET + SESS_POOL_TARGET) / 1024), -2)
      into MAX_SESSIONS;
    
    MAX_SESSIONS := GREAT(MAX_SESSIONS, 100);
    IF exec_mode = 0 THEN
        --修改cpu相关参数
        SP_SET_PARA_VALUE(2, 'WORKER_THREADS', v_cpus);
        SP_SET_PARA_VALUE(2, 'IO_THR_GROUPS', IO_THR_GROUPS);
        --将此参数改为0
        SP_SET_PARA_VALUE(2, 'GEN_SQL_MEM_RECLAIM', 0);
        --修改内存池相关参数
        SP_SET_PARA_VALUE(2, 'MAX_OS_MEMORY', 100);
        SP_SET_PARA_VALUE(2, 'MEMORY_POOL', MEMORY_POOL);
        SP_SET_PARA_VALUE(2, 'MEMORY_N_POOLS', MEMORY_N_POOLS);
        SP_SET_PARA_VALUE(2, 'MEMORY_TARGET', MEMORY_TARGET);
        --修改内存检测参数为1 	
        SP_SET_PARA_VALUE(2, 'MEMORY_MAGIC_CHECK', 1);
        --修改缓冲区相关参数
        SP_SET_PARA_VALUE(2, 'BUFFER', BUFFER);
        --新版本已去掉MAX_BUFFER参数，如果存在就修改
        IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'MAX_BUFFER') THEN
            SP_SET_PARA_VALUE(2, 'MAX_BUFFER', MAX_BUFFER);
        END IF;
        SP_SET_PARA_VALUE(2, 'BUFFER_POOLS', BUFFER_POOLS);
        SP_SET_PARA_VALUE(2, 'RECYCLE', RECYCLE);
        SP_SET_PARA_VALUE(2, 'RECYCLE_POOLS', RECYCLE_POOLS);
        --修改fast_pool相关参数，如果是dsc环境，适当放小，以免影响启动速度
        IF is_dsc = 1 THEN
            SP_SET_PARA_VALUE(2, 'FAST_POOL_PAGES', 10000);
            SP_SET_PARA_VALUE(2, 'FAST_ROLL_PAGES', 3000);
            SP_SET_PARA_VALUE(2, 'TASK_THREADS', 16);
            SP_SET_PARA_VALUE(2, 'DSC_INSERT_LOCK_ROWS', 0);
        ELSE
            SP_SET_PARA_VALUE(2, 'FAST_POOL_PAGES', FAST_POOL_PAGES);
            SP_SET_PARA_VALUE(2, 'FAST_ROLL_PAGES', FAST_ROLL_PAGES);
            SP_SET_PARA_VALUE(2, 'TASK_THREADS', TASK_THREADS);
            --如果不是dsc环境，开启热页动态加载，关闭预读
            SP_SET_PARA_VALUE(2, 'ENABLE_FREQROOTS', 1);
            SP_SET_PARA_VALUE(2, 'MULTI_PAGE_GET_NUM', 1);
            SP_SET_PARA_VALUE(2, 'PRELOAD_SCAN_NUM', 0);
            SP_SET_PARA_VALUE(2, 'PRELOAD_EXTENT_NUM', 0);
        END IF;
        --修改HASH相关参数
        SP_SET_PARA_VALUE(1, 'HJ_BUF_GLOBAL_SIZE', HJ_BUF_GLOBAL_SIZE);
        SP_SET_PARA_VALUE(1, 'HJ_BUF_SIZE', HJ_BUF_SIZE );
        SP_SET_PARA_VALUE(1, 'HAGR_BUF_GLOBAL_SIZE', HAGR_BUF_GLOBAL_SIZE);
        SP_SET_PARA_VALUE(1, 'HAGR_BUF_SIZE', HAGR_BUF_SIZE );
        --修改排序相关参数
        SP_SET_PARA_VALUE(2, 'SORT_FLAG', SORT_FLAG);
        SP_SET_PARA_VALUE(2, 'SORT_BLK_SIZE', SORT_BLK_SIZE);
        SP_SET_PARA_VALUE(2, 'SORT_BUF_SIZE', SORT_BUF_SIZE);
        SP_SET_PARA_VALUE(2, 'SORT_BUF_GLOBAL_SIZE', SORT_BUF_GLOBAL_SIZE);
        --修改其他内存参数
        SP_SET_PARA_VALUE(2, 'RLOG_POOL_SIZE', RLOG_POOL_SIZE);
        SP_SET_PARA_VALUE(2, 'CACHE_POOL_SIZE', CACHE_POOL_SIZE);
        SP_SET_PARA_VALUE(2, 'DICT_BUF_SIZE', DICT_BUF_SIZE);
        SP_SET_PARA_VALUE(2, 'VM_POOL_TARGET', VM_POOL_TARGET);
        SP_SET_PARA_VALUE(2, 'SESS_POOL_TARGET', SESS_POOL_TARGET);
        --修改实例相关参数
        SP_SET_PARA_VALUE(2, 'USE_PLN_POOL', 1);
        SP_SET_PARA_VALUE(2, 'ENABLE_MONITOR', 1);
        SP_SET_PARA_VALUE(2, 'SVR_LOG', 0);
        SP_SET_PARA_VALUE(2, 'TEMP_SIZE', 1024);
        SP_SET_PARA_VALUE(2, 'TEMP_SPACE_LIMIT', 102400);
        SP_SET_PARA_VALUE(2, 'MAX_SESSIONS', MAX_SESSIONS);
        SP_SET_PARA_VALUE(2, 'MAX_SESSION_STATEMENT', 20000);
        --性能要求高且大字段较少的业务场景建议设置为1，大字段多的场景设置为0
        if pk_cluster_mode = 1 then
            SP_SET_PARA_VALUE(2, 'PK_WITH_CLUSTER', 1);
        else
            SP_SET_PARA_VALUE(2, 'PK_WITH_CLUSTER', 0);
        end if;
        SP_SET_PARA_VALUE(2, 'ENABLE_ENCRYPT', 0);
        --修改优化器相关参数
        SP_SET_PARA_VALUE(2, 'OLAP_FLAG', 2);
        SP_SET_PARA_VALUE(2, 'VIEW_PULLUP_FLAG', 1);
        SP_SET_PARA_VALUE(2, 'OPTIMIZER_MODE', 1);
        SP_SET_PARA_VALUE(2, 'ADAPTIVE_NPLN_FLAG', 0);
        --禁用索引监控和位图索引
        SP_SET_PARA_VALUE(2, 'MONITOR_INDEX_FLAG', 2);
        SP_SET_PARA_VALUE(2, 'ENABLE_CREATE_BM_INDEX_FLAG', 0);
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 3 AND VER2 <= 163 THEN
            SP_SET_PARA_VALUE(2, 'OPTIMIZER_OR_NBEXP', 0);
        END IF;
        --3.175之前的版本BIND_PARAM_OPT_FLAG参数改为0
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 3 AND VER2 <= 175 THEN
            SP_SET_PARA_VALUE(2, 'BIND_PARAM_OPT_FLAG', 0);
        END IF;
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 3 AND VER2 <= 153 THEN
            IF EXISTS (SELECT *
                   FROM V\$DM_INI
                  WHERE PARA_NAME = 'GROUP_OPT_FLAG'
                    and DEFAULT_VALUE = 60) THEN
                SP_SET_PARA_VALUE(2, 'GROUP_OPT_FLAG', 52);
            END IF;
        END IF;
        IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'MEM_POOL_EXTEND_MODE') THEN
            SP_SET_PARA_VALUE(2, 'MEM_POOL_EXTEND_MODE', 0);
        END IF;
        --8.1.4.189以前的版本OPERATION_NEW_MOTION参数改为0，隐藏参数
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 4 AND VER2 < 189 THEN
            PRINT '-- 8.1.4.189以前的版本OPERATION_NEW_MOTION参数改为0，隐藏参数需要在dm.ini中添加 OPERATION_NEW_MOTION=0';
        END IF;
        -- V8.1.4.111以前的版本HASH_JOIN_LOOP_TIMES参数改为1，隐藏参数
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 4 AND VER2 < 111 THEN
            PRINT '-- V8.1.4.111以前的版本HASH_JOIN_LOOP_TIMES参数改为1，隐藏参数需要在dm.ini中添加 HASH_JOIN_LOOP_TIMES=1';
        END IF;
        --开启并行PURGE
        SP_SET_PARA_VALUE(2, 'PARALLEL_PURGE_FLAG', 1);
        --开启手动并行
        SP_SET_PARA_VALUE(2, 'PARALLEL_POLICY', 2);
        SP_SET_PARA_DOUBLE_VALUE(2, 'UNDO_RETENTION', UNDO_RETENTION);
        --UNDO_RETENTION如果放大，可以适当调大UNDO_EXTENT_NUM。负载高的时候，减少文件系统的申请/释放操作。
        SP_SET_PARA_VALUE(2, 'UNDO_EXTENT_NUM', 16);
        --开启SQL 注入HINT功能
        SP_SET_PARA_VALUE(2, 'ENABLE_INJECT_HINT', 1);
        SP_SET_PARA_VALUE(2, 'FAST_LOGIN', 1);
        SP_SET_PARA_VALUE(2, 'BTR_SPLIT_MODE', 1);
        --关闭参数监控
        SP_SET_PARA_VALUE(2, 'ENABLE_MONITOR_BP', 0);
        --SLCT_OPT_FLAG参数设置为0
        IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'SLCT_OPT_FLAG') THEN
            SP_SET_PARA_VALUE(1, 'SLCT_OPT_FLAG', 0);
        END IF;
        IF is_dsc = 1 THEN
            SP_SET_PARA_VALUE(2, 'ENABLE_FREQROOTS', 0);
            --2025Q3 8.1.4.169以前的版本DSC关闭数据页预加载参数，8.1.4.169之后打开
            IF VER0 = 8 and VER1 > 0 AND VER1 <= 4 AND VER2 < 169 THEN
                SP_SET_PARA_VALUE(2, 'MULTI_PAGE_GET_NUM', 1);
                SP_SET_PARA_VALUE(2, 'PRELOAD_SCAN_NUM', 0);
                SP_SET_PARA_VALUE(2, 'PRELOAD_EXTENT_NUM', 0);
            ELSE
                SP_SET_PARA_VALUE(2, 'MULTI_PAGE_GET_NUM', 16);
                SP_SET_PARA_VALUE(2, 'PRELOAD_SCAN_NUM', 4);
                SP_SET_PARA_VALUE(2, 'PRELOAD_EXTENT_NUM', 5);
            END IF;
            SP_SET_PARA_VALUE(2, 'DSC_N_POOLS', MEMORY_N_POOLS);
            IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'DSC_GBS_REVOKE_OPT') THEN
                SP_SET_PARA_VALUE(2, 'DSC_GBS_REVOKE_OPT', 0);
            END IF;
            SP_SET_PARA_VALUE(2, 'DSC_HALT_SYNC', 0);
            SP_SET_PARA_VALUE(2, 'DSC_N_CTLS', 50000);
            SP_SET_PARA_VALUE(2, 'DSC_ENABLE_MONITOR', 0);
            SP_SET_PARA_VALUE(2, 'TRX_DICT_LOCK_NUM', 5);
            SP_SET_PARA_VALUE(2, 'DIRECT_IO', 1);
        END IF;
    ELSE
        --修改cpu相关参数
        PRINT 'SP_SET_PARA_VALUE(2,''WORKER_THREADS'',' || v_cpus || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''IO_THR_GROUPS'',' || IO_THR_GROUPS || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''GEN_SQL_MEM_RECLAIM'',0);';
        --修改内存池相关参数
        PRINT 'SP_SET_PARA_VALUE(2,''MAX_OS_MEMORY'',       ' || 100 || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''MEMORY_POOL'',         ' || MEMORY_POOL || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''MEMORY_N_POOLS'',      ' || MEMORY_N_POOLS || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''MEMORY_TARGET'',       ' || MEMORY_TARGET || ');';
        --修改缓冲区相关参数
        PRINT 'SP_SET_PARA_VALUE(2,''BUFFER'',              ' || BUFFER || ');';
        --新版本已去掉MAX_BUFFER参数，如果存在就修改
        IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'MAX_BUFFER') THEN
            PRINT 'SP_SET_PARA_VALUE(2,''MAX_BUFFER'',          ' || MAX_BUFFER || ');';
        END IF;
        PRINT 'SP_SET_PARA_VALUE(2,''BUFFER_POOLS'',        ' || BUFFER_POOLS || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''RECYCLE'',            ' || RECYCLE || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''RECYCLE_POOLS'',       ' || RECYCLE_POOLS || ');';
        --修改fast_pool相关参数，如果是dsc环境，适当放小，以免影响启动速度
        IF is_dsc = 1 THEN
            PRINT 'SP_SET_PARA_VALUE(2,''FAST_POOL_PAGES'',  10000);';
            PRINT 'SP_SET_PARA_VALUE(2,''FAST_ROLL_PAGES'',   3000);';
            PRINT 'SP_SET_PARA_VALUE(2,''TASK_THREADS'',     16);';
            PRINT 'SP_SET_PARA_VALUE(2,''DSC_INSERT_LOCK_ROWS'', 0);';
        ELSE
            PRINT 'SP_SET_PARA_VALUE(2,''FAST_POOL_PAGES'',     ' || FAST_POOL_PAGES || ');';
            PRINT 'SP_SET_PARA_VALUE(2,''FAST_ROLL_PAGES'',     ' || FAST_ROLL_PAGES || ');';
            --如果不是dsc环境，开启热页动态加载，关闭预读
            PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_FREQROOTS'',1);';
            PRINT 'SP_SET_PARA_VALUE(2,''MULTI_PAGE_GET_NUM'',1);';
            PRINT 'SP_SET_PARA_VALUE(2,''PRELOAD_SCAN_NUM'',0);';
            PRINT 'SP_SET_PARA_VALUE(2,''PRELOAD_EXTENT_NUM'',0);';
            PRINT 'SP_SET_PARA_VALUE(2,''TASK_THREADS'',' || TASK_THREADS || ');';
        END IF;
        --修改内存检测参数为1 	
        PRINT 'SP_SET_PARA_VALUE(2,''MEMORY_MAGIC_CHECK'',       1);';
        --修改HASH相关参数
        PRINT 'SP_SET_PARA_VALUE(1,''HJ_BUF_GLOBAL_SIZE'',  ' || HJ_BUF_GLOBAL_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(1,''HJ_BUF_SIZE'',        ' || HJ_BUF_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(1,''HAGR_BUF_GLOBAL_SIZE'',' || HAGR_BUF_GLOBAL_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(1,''HAGR_BUF_SIZE'',     ' || HAGR_BUF_SIZE || ');';
        --修改排序相关参数
        PRINT 'SP_SET_PARA_VALUE(2,''SORT_FLAG'',' || SORT_FLAG || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''SORT_BLK_SIZE'',' || SORT_BLK_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''SORT_BUF_SIZE'',       ' || SORT_BUF_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''SORT_BUF_GLOBAL_SIZE'',       ' || SORT_BUF_GLOBAL_SIZE || ');';
        --修改其他内存参数
        PRINT 'SP_SET_PARA_VALUE(2,''RLOG_POOL_SIZE'',      ' || RLOG_POOL_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''CACHE_POOL_SIZE'',     ' || CACHE_POOL_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''DICT_BUF_SIZE'',       ' || DICT_BUF_SIZE || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''VM_POOL_TARGET'',      ' || VM_POOL_TARGET || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''SESS_POOL_TARGET'',    ' || SESS_POOL_TARGET || ');';
        --修改实例相关参数
        PRINT 'SP_SET_PARA_VALUE(2,''USE_PLN_POOL'',        1);';
        PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_MONITOR'',      1);';
        PRINT 'SP_SET_PARA_VALUE(2,''SVR_LOG'',             0);';
        PRINT 'SP_SET_PARA_VALUE(2,''TEMP_SIZE'',           1024);';
        PRINT 'SP_SET_PARA_VALUE(2,''TEMP_SPACE_LIMIT'',    102400);';
        PRINT 'SP_SET_PARA_VALUE(2,''MAX_SESSIONS'',        ' || MAX_SESSIONS || ');';
        PRINT 'SP_SET_PARA_VALUE(2,''MAX_SESSION_STATEMENT'', 20000);';
        --性能要求高且大字段较少的业务场景建议设置为1，大字段多的场景设置为0
        if pk_cluster_mode = 1 then
            PRINT 'SP_SET_PARA_VALUE(2,''PK_WITH_CLUSTER'',		1);';
        else
            PRINT 'SP_SET_PARA_VALUE(2,''PK_WITH_CLUSTER'',		0);';
        end if;
        PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_ENCRYPT'',0);';
        --修改优化器相关参数
        PRINT 'SP_SET_PARA_VALUE(2,''OLAP_FLAG'',2);';
        PRINT 'SP_SET_PARA_VALUE(2,''VIEW_PULLUP_FLAG'',1);';
        PRINT 'SP_SET_PARA_VALUE(2,''OPTIMIZER_MODE'',1);';
        PRINT 'SP_SET_PARA_VALUE(2,''ADAPTIVE_NPLN_FLAG'',0);';
        --禁用索引监控和位图索引
        PRINT 'SP_SET_PARA_VALUE(2,''MONITOR_INDEX_FLAG'',2);';
        PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_CREATE_BM_INDEX_FLAG'',0);';
        --3.163 之前的版本OPTIMIZER_OR_NBEXP不能包含16
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 3 AND VER2 < 163 THEN
            PRINT 'SP_SET_PARA_VALUE(2,''OPTIMIZER_OR_NBEXP'',0);';
        END IF;
        --3.175之前的版本BIND_PARAM_OPT_FLAG参数改为0
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 3 AND VER2 <= 175 THEN
            PRINT 'SP_SET_PARA_VALUE(2,''BIND_PARAM_OPT_FLAG'',0);';
        END IF;
        --3.153 之前的版本GROUP_OPT_FLAG不能包含8
        IF VER0 = 8 and VER1 > 0 AND VER1 <= 3 AND VER2 < 153 THEN
            IF EXISTS (SELECT *
                   FROM V\$DM_INI
                  WHERE PARA_NAME = 'GROUP_OPT_FLAG'
                    and DEFAULT_VALUE = 60) THEN
                PRINT 'SP_SET_PARA_VALUE(2,''GROUP_OPT_FLAG'',52);';
            END IF;
        END IF;
        IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'MEM_POOL_EXTEND_MODE') THEN
            PRINT 'SP_SET_PARA_VALUE(2,''MEM_POOL_EXTEND_MODE'',0);';
        END IF;
        --开启并行PURGE
        PRINT 'SP_SET_PARA_VALUE(2,''PARALLEL_PURGE_FLAG'',1);';
        --开启手动并行
        PRINT 'SP_SET_PARA_VALUE(2,''PARALLEL_POLICY'',2);';
        PRINT 'SP_SET_PARA_DOUBLE_VALUE(2,''UNDO_RETENTION'',' || UNDO_RETENTION || ');';
        --UNDO_RETENTION如果放大，可以适当调大UNDO_EXTENT_NUM。负载高的时候，减少文件系统的申请/释放操作。
        PRINT 'SP_SET_PARA_VALUE(2,''UNDO_EXTENT_NUM'',16);';
        --开启INJECT HINT功能
        PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_INJECT_HINT'',1);';
        PRINT 'SP_SET_PARA_VALUE(2,''BTR_SPLIT_MODE'',1);';
        PRINT 'SP_SET_PARA_VALUE(2,''FAST_LOGIN'',1);';
        --关闭参数监控
        PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_MONITOR_BP'',0);';
        --SLCT_OPT_FLAG参数设置为0
        IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'SLCT_OPT_FLAG') THEN
            PRINT 'SP_SET_PARA_VALUE(1,''SLCT_OPT_FLAG'',0);';
        END IF;
        IF is_dsc = 1 THEN
            PRINT 'SP_SET_PARA_VALUE(2,''ENABLE_FREQROOTS'',0);';
            --2025Q3 8.1.4.169以前的版本DSC关闭数据页预加载参数，8.1.4.169之后打开
            IF VER0 = 8 and VER1 > 0 AND VER1 <= 4 AND VER2 < 169 THEN
                PRINT 'SP_SET_PARA_VALUE(2,''MULTI_PAGE_GET_NUM'',1);';
                PRINT 'SP_SET_PARA_VALUE(2,''PRELOAD_SCAN_NUM'',0);';
                PRINT 'SP_SET_PARA_VALUE(2,''PRELOAD_EXTENT_NUM'',0);';
            ELSE
                PRINT 'SP_SET_PARA_VALUE(2,''MULTI_PAGE_GET_NUM'',16);';
                PRINT 'SP_SET_PARA_VALUE(2,''PRELOAD_SCAN_NUM'',4);';
                PRINT 'SP_SET_PARA_VALUE(2,''PRELOAD_EXTENT_NUM'',5);';
            END IF;
            PRINT 'SP_SET_PARA_VALUE(2,''DSC_N_POOLS'',' || MEMORY_N_POOLS || ');';
            IF EXISTS (SELECT * FROM V\$DM_INI WHERE PARA_NAME = 'DSC_GBS_REVOKE_OPT') THEN
                PRINT 'SP_SET_PARA_VALUE(2,''DSC_GBS_REVOKE_OPT'',0);';
            END IF;
            PRINT 'SP_SET_PARA_VALUE(2,''DSC_HALT_SYNC'',0);';
            PRINT 'SP_SET_PARA_VALUE(2,''DSC_N_CTLS'',50000);';
            PRINT 'SP_SET_PARA_VALUE(2,''DSC_ENABLE_MONITOR'',0);';
            PRINT 'SP_SET_PARA_VALUE(2,''TRX_DICT_LOCK_NUM'',5);';
            PRINT 'SP_SET_PARA_VALUE(2,''DIRECT_IO'',1);';
            --8.1.4.189以前的版本OPERATION_NEW_MOTION参数改为0，隐藏参数
            IF VER0 = 8 and VER1 > 0 AND VER1 <= 4 AND VER2 < 189 THEN
                PRINT '-- 8.1.4.189以前的版本OPERATION_NEW_MOTION参数改为0，隐藏参数需要在dm.ini中添加 OPERATION_NEW_MOTION=0';
            END IF;
            -- V8.1.4.111以前的版本HASH_JOIN_LOOP_TIMES参数改为1，隐藏参数
            IF VER0 = 8 and VER1 > 0 AND VER1 <= 4 AND VER2 < 111 THEN
                PRINT '-- V8.1.4.111以前的版本HASH_JOIN_LOOP_TIMES参数改为1，隐藏参数需要在dm.ini中添加 HASH_JOIN_LOOP_TIMES=1';
            END IF;
        END IF;
    END IF;
    select MEMORY_TARGET + BUFFER + RECYCLE + HJ_BUF_GLOBAL_SIZE + HAGR_BUF_GLOBAL_SIZE + CACHE_POOL_SIZE + DICT_BUF_SIZE + SORT_BUF_GLOBAL_SIZE + RLOG_POOL_SIZE + MAX_SESSIONS * ((VM_POOL_TARGET + SESS_POOL_TARGET) / 1024);

exception
when others then
    raise_application_error ( - 20001, substr( ' 执行失败, ' || SQLCODE || ' ' || SQLERRM || ' ' || dbms_utility.format_error_backtrace, 1, 400));
    << return_2000 >> null;
end;
/
exit;
EOF
	)
	# 6：写入 conf_dmini.sql
	run_cmd printf "%s\n" "$sql_text" | run_cmd tee "$conf_dmini" >/dev/null
	run_cmd chown dmdba:dinstall "$conf_dmini"
	run_cmd ls -lh "${conf_dmini}"
	# 7：修改除优化脚本之外的参数
	sql_text=$(
		cat <<-'EOF'
			SET ECHO OFF
			SET TIMING OFF
			SET FEEDBACK OFF
			SET LINESHOW OFF
			SET SERVEROUTPUT ON
			DECLARE
			    CURSOR c IS
			        SELECT PARA_NAME
			          FROM V\$DM_INI
			         WHERE PARA_NAME IN ('BEXP_CALC_ST_FLAG',
			                             'TRX_VIEW_POLICY');
			BEGIN
			    FOR r IN c
			    LOOP
			        CASE r.PARA_NAME
			          WHEN 'BEXP_CALC_ST_FLAG' THEN 
			            SP_SET_PARA_VALUE(1, 'BEXP_CALC_ST_FLAG', 0);
			            PRINT 'SP_SET_PARA_VALUE(1,''BEXP_CALC_ST_FLAG'',0);';
			          WHEN 'TRX_VIEW_POLICY' THEN 
			            SP_SET_PARA_VALUE(1, 'TRX_VIEW_POLICY', 1);
			            PRINT 'SP_SET_PARA_VALUE(1,''TRX_VIEW_POLICY'',1);';
			        END CASE;
			    END LOOP;
			END;
			/
			exit;
		EOF
	)
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${conf_dmini_other}" >/dev/null
EOF
	run_cmd ls -lh "${conf_dmini_other}"
	# 8：兼容模式脚本（如需要）
	if [[ $compatible_mode -ne 0 ]]; then
		printf "\n"
		color_printf blue "创建兼容其他数据库脚本:"
		printf "\n"
		sql_text=$(
			cat <<'EOF'
SET ECHO OFF
SET TIMING OFF
SET FEEDBACK OFF
SET LINESHOW OFF
SET PAGESIZE 1000
DECLARE
    v_compatible_mode NUMBER; -- 用于存储COMPATIBLE_MODE的值
    TYPE t_command_array IS TABLE OF VARCHAR2(1000); -- 定义一个命令数组类型
    v_commands t_command_array; -- 用于存储命令的数组
BEGIN
    -- 从v\$dm_ini视图中选择COMPATIBLE_MODE的值
    SELECT para_value
    INTO v_compatible_mode
    FROM v\$dm_ini
    WHERE para_name = 'COMPATIBLE_MODE';
    -- 检查COMPATIBLE_MODE的值
    IF v_compatible_mode = 2 THEN
        -- 初始化命令数组
        v_commands := t_command_array(
            'SP_SET_PARA_VALUE(1, ''ORDER_BY_NULLS_FLAG'', 1)',
            'SP_SET_PARA_VALUE(2, ''CALC_AS_DECIMAL'', 1)',
            'SP_SET_PARA_VALUE(1, ''ENABLE_SEQ_REUSE'', 1)',
            'SP_SET_PARA_VALUE(1, ''ENABLE_PL_SYNONYM'', 1)',
            'SP_SET_PARA_VALUE(1, ''ENABLE_BLOB_CMP_FLAG'', 1)',
            'SP_SET_PARA_VALUE(2, ''NUMBER_MODE'', 1)',
            'SP_SET_PARA_VALUE(2, ''VIEW_ACCESS_MODE'', 1)'
        );
        -- 执行所有命令
        FOR i IN 1 .. v_commands.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE('已执行命令修改参数: ' || v_commands(i));
            EXECUTE IMMEDIATE v_commands(i);
        END LOOP;
        -- 输出执行结果
        DBMS_OUTPUT.PUT_LINE('参数设置已成功执行。');
    ELSE
        DBMS_OUTPUT.PUT_LINE('COMPATIBLE_MODE 不等于 2，跳过参数设置。');
    END IF;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('没有找到COMPATIBLE_MODE的值。');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('发生错误: ' || SQLERRM);
END;
/
exit;
EOF
		)
		run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${conf_com_mode_script}" >/dev/null
EOF
		run_cmd ls -lh "${conf_com_mode_script}"
	fi
	# 9：创建查询优化结果脚本
	printf "\n"
	color_printf blue "创建数据库优化结果查询脚本:"
	printf "\n"
	sql_text=$(
		cat <<'EOF'
SET ECHO OFF
SET TIMING OFF
SET FEEDBACK OFF
SET LINESHOW OFF
SET PAGESIZE 1000
select PARA_NAME name,
       PARA_VALUE para_value,
       FILE_VALUE file_value
  from v\$DM_INI
 WHERE PARA_NAME IN ('WORKER_THREADS',
                     'TASK_THREADS',
                     'IO_THR_GROUPS',
                     'MAX_OS_MEMORY',
                     'MEMORY_POOL',
                     'MEMORY_N_POOLS',
                     'MEMORY_TARGET',
                     'BUFFER',
                     'MAX_BUFFER',
                     'BUFFER_POOLS',
                     'RECYCLE',
                     'RECYCLE_POOLS',
                     'FAST_POOL_PAGES',
                     'FAST_ROLL_PAGES',
                     'MEMORY_MAGIC_CHECK',
                     'HJ_BUF_GLOBAL_SIZE',
                     'HJ_BUF_SIZE',
                     'HAGR_BUF_GLOBAL_SIZE',
                     'HAGR_BUF_SIZE',
                     'SORT_FLAG',
                     'SORT_BLK_SIZE',
                     'SORT_BUF_SIZE',
                     'SORT_BUF_GLOBAL_SIZE',
                     'RLOG_POOL_SIZE',
                     'CACHE_POOL_SIZE',
                     'DICT_BUF_SIZE',
                     'VM_POOL_TARGET',
                     'SESS_POOL_TARGET',
                     'USE_PLN_POOL',
                     'ENABLE_MONITOR',
                     'SVR_LOG',
                     'TEMP_SIZE',
                     'TEMP_SPACE_LIMIT',
                     'MAX_SESSIONS',
                     'MAX_SESSION_STATEMENT',
                     'PK_WITH_CLUSTER',
                     'ENABLE_ENCRYPT',
                     'OLAP_FLAG',
                     'VIEW_PULLUP_FLAG',
                     'OPTIMIZER_MODE',
                     'ADAPTIVE_NPLN_FLAG',
                     'PARALLEL_PURGE_FLAG',
                     'PARALLEL_POLICY',
                     'UNDO_EXTENT_NUM',
                     'ENABLE_INJECT_HINT',
                     'BTR_SPLIT_MODE',
                     'FAST_LOGIN',
                     'ENABLE_MONITOR_BP',
                     'MULTI_PAGE_GET_NUM',
                     'PRELOAD_SCAN_NUM',
                     'PRELOAD_EXTENT_NUM',
                     'DSC_N_POOLS',
                     'DSC_GBS_REVOKE_OPT',
                     'DSC_HALT_SYNC',
                     'DSC_N_CTLS',
                     'DSC_ENABLE_MONITOR',
                     'TRX_DICT_LOCK_NUM',
                     'ENABLE_FREQROOTS',
                     'DIRECT_IO')
union all
select 'MAL_CHECK_INTERVAL' ,
       'placeholder',
       CASE WHEN COUNT(MAL_CHECK_INTERVAL)=0 THEN 'NULL' ELSE (SELECT MAL_CHECK_INTERVAL FROM V\$MAL_SYS LIMIT 1) END
  FROM V\$MAL_SYS
union all
select 'MAL_CONN_FAIL_INTERVAL' ,
       'placeholder',
       CASE WHEN COUNT(MAL_CONN_FAIL_INTERVAL)=0 THEN 'NULL' ELSE (SELECT MAL_CONN_FAIL_INTERVAL FROM V\$MAL_SYS LIMIT 1) END
  from V\$MAL_SYS
union all
select 'MAL_BUF_SIZE' ,
       'placeholder',
       CASE WHEN COUNT(MAL_BUF_SIZE)=0 THEN 'NULL' ELSE (SELECT MAL_BUF_SIZE FROM V\$MAL_SYS LIMIT 1) END
  from V\$MAL_SYS
union all
select 'MAL_SYS_BUF_SIZE' ,
       'placeholder',
       CASE WHEN COUNT(MAL_SYS_BUF_SIZE)=0 THEN 'NULL' ELSE (SELECT MAL_SYS_BUF_SIZE FROM V\$MAL_SYS LIMIT 1) END
  from V\$MAL_SYS
union all
select 'MAL_COMPRESS_LEVEL' ,
       'placeholder',
       CASE WHEN COUNT(MAL_COMPRESS_LEVEL)=0 THEN 'NULL' ELSE (SELECT MAL_COMPRESS_LEVEL FROM V\$MAL_SYS LIMIT 1) END
  from V\$MAL_SYS
union all
select 'MAL_TEMP_PATH' ,
       'placeholder',
       CASE WHEN COUNT(MAL_TEMP_PATH)=0 THEN 'NULL' ELSE (SELECT MAL_TEMP_PATH||' ' FROM V\$MAL_SYS LIMIT 1) END
  from V\$MAL_SYS
union all
select 'MAL_VPOOL_SIZE' ,
       'placeholder',
       CASE WHEN COUNT(MAL_VPOOL_SIZE)=0 THEN 'NULL' ELSE (SELECT MAL_VPOOL_SIZE FROM V\$MAL_SYS LIMIT 1) END
  from V\$MAL_SYS
union all
  select 'MAL_INST_NAME' ,
         'placeholder',
         listagg(to_char(MAL_INST_NAME ),',') within GROUP (order by MAL_INST_NAME)
    from V\$dm_MAL_ini
union all
  select 'MAL_HOST' ,
         'placeholder',
         listagg(to_char(MAL_HOST ),',') within GROUP (order by MAL_HOST)
    from V\$dm_MAL_ini
union all
  select 'MAL_PORT' ,
         'placeholder',
         listagg(to_char(MAL_PORT ),',') within GROUP (order by MAL_PORT)
    from V\$dm_MAL_ini
union all
  select 'MAL_INST_HOST' ,
         'placeholder',
         listagg(to_char(MAL_INST_HOST ),',') within GROUP (order by MAL_INST_HOST)
    from V\$dm_MAL_ini
union all
  select 'MAL_INST_PORT' ,
         'placeholder',
         listagg(to_char(MAL_INST_PORT ),',') within GROUP (order by MAL_INST_PORT)
    from V\$dm_MAL_ini
union all
  select 'MAL_DW_PORT' ,
         'placeholder',
         listagg(to_char(MAL_DW_PORT ),',') within GROUP (order by MAL_DW_PORT)
    from V\$dm_MAL_ini
UNION ALL
select 'ARCH_DEST',
       'placeholder',
       CASE WHEN COUNT(ARCH_DEST)=0 THEN 'NULL' ELSE (SELECT ARCH_DEST FROM V\$dm_arch_ini LIMIT 1) END
  from V\$dm_arch_ini;
exit;
EOF
	)
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${query_script}" >/dev/null
EOF
	run_cmd ls -lh "${query_script}"
	# 10：创建统计信息收集脚本
	printf "\n"
	color_printf blue "创建数据库搜集统计信息脚本:"
	printf "\n"
	sql_text=$(
		cat <<'EOF'
SET ECHO OFF
SET TIMING OFF
SET FEEDBACK OFF
SET LINESHOW OFF
SET SERVEROUTPUT ON
--call SP_DROP_JOB('statistics');
call SP_CREATE_JOB('statistics',1,0,'',0,0,'',0,'每周六凌晨2点开始收集所有列统计信息');
call SP_JOB_CONFIG_START('statistics');
call SP_ADD_JOB_STEP('statistics', 'statistics1', 0, 'begin
    for rs in (select ''sf_set_SESSION_para_value(''''HAGR_HASH_SIZE'''',
                      (select cast( case when max(table_rowcount(owner,table_name))<=(select max_value from v\$dm_ini where para_Name=''''HAGR_HASH_SIZE'''') and max(table_rowcount(owner,table_name))>=( select min_value from v\$dm_ini where para_Name=''''HAGR_HASH_SIZE'''') then max(table_rowcount(owner,table_name))
                                         when max(table_rowcount(owner,table_name))<( select min_value from v\$dm_ini where para_Name=''''HAGR_HASH_SIZE'''') then (select min_value from v\$dm_ini where para_Name=''''HAGR_HASH_SIZE'''')
                                         else (select max_value from v\$dm_ini where para_Name=''''HAGR_HASH_SIZE'''')
                                    end as bigint)
                      from dba_tables
                      where owner=''''''||NAME||''''''));'' sql1,
                      ''DBMS_STATS.GATHER_SCHEMA_STATS(''''''||NAME||'''''',100,TRUE,''''FOR ALL COLUMNS SIZE AUTO'''',1,''''AUTO'''',NULL,NULL,NULL,NO_INVALIDATE => FALSE);'' sql2
              from SYS.SYSOBJECTS
              where TYPE$=''SCH'')
    loop
        execute immediate rs.sql1;
        execute immediate rs.sql2;
    end loop;
end;', 0, 0, 0, 0, NULL, 0);
call SP_ADD_JOB_SCHEDULE('statistics', 'statistics1', 1, 2, 1, 64, 0, '02:00:00', NULL, '2021-06-09 22:54:37', NULL, '');
call SP_JOB_CONFIG_COMMIT('statistics');
SELECT NAME,
       describe
  FROM sysjob.sysjobs
  where name = 'statistics';
exit;
EOF
	)
	run_cmd su - dmdba <<EOF
printf "%s\n" "$sql_text" | tee "${conf_stat_script}" >/dev/null
EOF
	run_cmd ls -lh "${conf_stat_script}"
}
#========================================================
# 函数：creat_arch
# 作用：根据部署模式配置数据库归档
#========================================================
function creat_arch() {
	# 1：打印开始提示
	log_print "配置数据库归档"
	# 2：单机模式直接本地执行
	if [[ $dm_install_mode == "single" ]]; then
		execute_script "SYSDBA" "$sysdba_pwd_temp" "conf_arch.sql"
		sysinfo[dmIni]="ARCH_INI = 1"
		sysinfo[dmarchIni]=$(escape_string "$(cat "$data_dir/$db_name/dmarch.ini")")
		return
	fi
	# 3：仅 DSC/DSCM 继续
	[[ $dm_install_mode =~ ^(dsc|dscm)$ ]] || return
	# 4：等待实例 OPEN
	# shellcheck disable=SC2155
	local config_file="${data_dir}/${dsc_db_name}/${dsc_ep_name}$(printf '%02d' "$node_num")_config/dm.ini"
	if pgrep -f "$config_file" &>/dev/null; then
		check_db_status "OPEN"
	fi
	# 5：逐节点配置归档
	local idx node
	for idx in "${!all_pubip_arr[@]}"; do
		node=${all_pubip_arr[$idx]}
		color_printf blue "在节点 $node 创建归档"
		printf "\n"
		# 远程执行归档脚本
		exec_ssh_cmd "$node" "scripts_dir='${scripts_dir}';
env_dm_home='${env_dm_home}';
port_num='${port_num}';
sysdba_pwd_temp='${sysdba_pwd_temp}';
$(declare -f run_cmd execute_script)
execute_script SYSDBA '${sysdba_pwd_temp}' conf_arch.sql
      " || {
			color_printf red "节点 $node 归档配置失败"
			continue
		}
		printf "\n"
		# 5.1：DSCM 非本地归档且指定 arch_disk 时追加 ASM 参数
		if [[ $dm_install_mode == "dscm" && $is_local_arch =~ [Nn] ]]; then
			# shellcheck disable=SC2155
			local dmarch_ini="${data_dir}/${dsc_db_name}/${dsc_ep_name}$(printf '%02d' $((idx + 1)))_config/dmarch.ini"
			exec_ssh_cmd "$node" "sed -i '6iARCH_ASM_MIRROR=$arch_asm_mirror\nARCH_ASM_STRIPING=$arch_asm_striping\n' '$dmarch_ini'" ||
				color_printf red "节点 $node dmarch.ini 追加 ASM 参数失败"
			printf "\n"
		fi
	done
}
#========================================================
# 函数：optimize_dmdb
# 作用：完成归档、备份、统计信息、参数优化及重启
#========================================================
function optimize_dmdb() {
	local WAIT_TIME=5 # 统一等待时间（秒）
	# 1：配置归档（如启用）
	[[ $enable_arch == "Y" && $dm_install_mode =~ ^(single|dsc|dscm)$ ]] && creat_arch
	# 2：备份相关配置与执行
	_backup_cfg() {
		local sed_expr ini
		if [[ $dm_install_mode =~ ^(single|datawatch|raft)$ ]]; then
			sed_expr="s|BAK_PATH[[:space:]]*=[[:space:]]*$data_dir/$db_name/bak|BAK_PATH = $backup_dir/$db_name/bak|g"
			sed -i "$sed_expr" "$data_dir/$db_name/dm.ini"
		else
			sed_expr="s|BAK_PATH[[:space:]]*=[[:space:]]*+DMDATA/dmdata/$dsc_db_name/bak|BAK_PATH = $backup_dir/$dsc_db_name/bak|g"
			for ((i = 0; i < ${#all_pubip_arr[@]}; i++)); do
				ini="$data_dir/$dsc_db_name/${dsc_ep_name}$(printf %02d $((i + 1)))_config/dm.ini"
				exec_ssh_cmd "${all_pubip_arr[$i]}" "sed -i \"$sed_expr\" \"$ini\""
			done
		fi
	}
	_do_backup() {
		[[ $enable_arch != "Y" ]] && return
		local ip
		case $dm_install_mode in
		datawatch | raft) ip=${all_pubip_arr[0]} ;;
		dsc | dscm)
			# shellcheck disable=SC2124
			ip="${all_pubip_arr[@]: -1}"
			;;
		*) ip=$(hostname -I | awk '{print $1}') ;;
		esac
		[[ -n $ip ]] || return
		log_print "在 $ip 创建备份"
		# shellcheck disable=SC2155
		local script=$([ "$backup_mode" -eq 1 ] && echo "conf_fullbackup.sql" || echo "conf_incrbackup.sql")
		execute_script "SYSDBA" "$sysdba_pwd_temp" "$script"
	}
	_backup_cfg
	_do_backup
	# 3：创建统计信息作业
	[[ $node_num -eq 1 ]] && {
		log_print "配置搜集统计信息作业"
		[[ $enable_arch == "N" ]] && execute_sql "SYSDBA" "$sysdba_pwd_temp" "SP_INIT_JOB_SYS(1);"
		execute_script "SYSDBA" "$sysdba_pwd_temp" "conf_statistics.sql"
	}
	# 4：执行基础参数优化
	log_print "优化数据库基础参数"
	execute_script "SYSDBA" "$sysdba_pwd_temp" "conf_dmini.sql"
	printf "\n"
	color_printf blue "修改额外的优化参数"
	printf "\n"
	# 5：执行脚本配置额外的参数
	execute_script "SYSDBA" "$sysdba_pwd_temp" "conf_dmini_other.sql"
	# 6：设置兼容模式（如需要）
	[[ $compatible_mode -ne 0 ]] && {
		log_print "打开兼容 $compatible_mode 参数"
		execute_sql "SYSDBA" "$sysdba_pwd_temp" "sp_set_para_value(2,'COMPATIBLE_MODE',$compatible_mode);"
		printf "\n"
		color_printf yellow "请查看脚本 ${scripts_dir}/conf_com_mod.sql 自行判断是否需要执行"
	}
	# 7：其他开关参数
	log_print "开启操作系统认证"
	[[ $os_auth =~ [Yy] ]] && execute_sql "SYSDBA" "$sysdba_pwd_temp" "sp_set_para_value(2,'ENABLE_LOCAL_OSAUTH',1);"
	log_print "开启SQLLOG日志"
	execute_sql "SYSDBA" "$sysdba_pwd_temp" "sp_set_para_value(2,'SVR_LOG',1);"
	log_print "修改整数计算方式"
	execute_sql "SYSDBA" "$sysdba_pwd_temp" "sp_set_para_value(2,'CALC_AS_DECIMAL',1);"
	# 8：重启数据库使参数生效
	log_print "重启数据库，优化参数生效"
	if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
		local ini
		ini="$data_dir/$dsc_db_name/${dsc_ep_name}$(printf %02d "$node_num")_config/dm.ini"
		su - dmdba -c "$env_dm_home/bin/dmcssm $data_dir/$dsc_db_name/dmcssm.ini" <<EOF
ep stop GRP_$dsc_db_name
exit
EOF
		while pgrep -f "$ini" >/dev/null; do sleep $WAIT_TIME; done
		su - dmdba -c "$env_dm_home/bin/dmcssm $data_dir/$dsc_db_name/dmcssm.ini" <<EOF
ep startup GRP_$dsc_db_name
exit
EOF
		check_db_status "OPEN"
	else
		run_cmd su - dmdba -c "$DMS_RESTART_CMD"
	fi
}
#========================================================
# 函数：conf_hugepage
# 作用：配置系统 HugePages
#========================================================
function conf_hugepage() {
	local KERN HPG_SZ NUM_PG=0 MIN_PG HUGETLB_POOL
	# 1：检查 HugePages 支持
	HPG_SZ=$(awk '/Hugepagesize/ {print $2}' /proc/meminfo)
	[[ -z "$HPG_SZ" ]] && {
		printf "\033[33m当前系统不支持HugePages！\033[0m\n"
		return 1
	}
	# 2：计算所需 HugePages 数量
	while read -r SEG_BYTES; do
		MIN_PG=$((SEG_BYTES / (HPG_SZ * 1024)))
		((MIN_PG > 0)) && NUM_PG=$((NUM_PG + MIN_PG + 1))
	done <<<"$(ipcs -m | awk '{print $5}' | grep -o '[0-9]*')"
	# 3：验证最小内存要求
	((NUM_PG * HPG_SZ * 1024 < 100000000)) && {
		printf "\033[33m共享内存段不足100MB，无法配置HugePages。\033[0m\n"
		return 1
	}
	# 4：根据内核版本写入 sysctl
	KERN=$(uname -r | awk -F. '{printf "%d.%d",$1,$2}')
	if [[ "$KERN" == "2.4" ]]; then
		HUGETLB_POOL=$((NUM_PG * HPG_SZ / 1024))
		sysctl -w "vm.hugetlb_pool=$HUGETLB_POOL" >/dev/null 2>&1
		printf "vm.hugetlb_pool=%s\n" "$HUGETLB_POOL" >>/etc/sysctl.conf
	else
		sysctl -w "vm.nr_hugepages=$NUM_PG" >/dev/null 2>&1
		printf "vm.nr_hugepages=%s\n" "$NUM_PG" >>/etc/sysctl.conf
	fi
	# 5：集群其他节点同步配置
	if [[ "$dm_install_mode" =~ ^(datawatch|raft|dsc|dscm)$ ]]; then
		for ip in "${all_pubip_arr[@]:1}"; do
			exec_ssh_cmd "$ip" "$(declare -f conf_hugepage); conf_hugepage"
		done
	fi
	# 6：打印当前 HugePages 信息
	grep HugePages_Total /proc/meminfo
}
#========================================================
# 函数：mod_db_pwd
# 作用：修改管理员密码并执行后续脚本
#========================================================
function mod_db_pwd() {
	log_print "修改管理员用户密码"
	# 0. 前置检查
	check_db_status "OPEN"
	# 1. 内部工具函数
	_gen_alter_sql() {
		local user=$1 passwd=$2 file=$3 alter_user_sql
		alter_user_sql=$(
			cat <<EOF
set timing off
set lineshow off
set feedback off
set define off
-- 修改 ${user} 密码
alter user \"${user}\" identified by \"${passwd}\" password_policy 31 limit failed_login_attemps unlimited, password_life_time unlimited, password_lock_time unlimited;
-- 查看密码策略
select so.name,
       su.failed_num,
       su.life_time,
       su.lock_time
  from sysobjects so,
       sysusers su
 where so.id = su.id
   and so.name = '${user}';
exit;
EOF
		)
		run_cmd su - dmdba <<EOF
printf "%s\n" "$alter_user_sql" | tee "${file}" >/dev/null
EOF
	}
	_change_one_pwd() {
		local user=$1 input_pwd=$2 sql_file=$3
		local final_pwd
		case $user in
		SYSDBA) final_pwd=${sysdba_pwd:-$(gen_password)} ;;
		SYSSSO) final_pwd=${syssso_pwd:-$(gen_password)} ;;
		SYSAUDITOR) final_pwd=${sysaud_pwd:-$(gen_password)} ;;
		esac
		sysinfo[$user]=$final_pwd
		_gen_alter_sql "$user" "$final_pwd" "${scripts_dir}/${sql_file}"
		printf "\n"
		execute_script "$user" "$input_pwd" "$sql_file"
		printf "\n"
		printf "已修改 %s 密码：%s\n" "${user}" "$(color_printf blue "${final_pwd}")"
	}
	# 2. 批量修改密码
	_change_one_pwd "SYSDBA" "$sysdba_pwd_temp" "alter_sysdba.sql"
	[[ $db_flag == "true" ]] && _change_one_pwd "SYSSSO" "$syssso_pwd_temp" "alter_syssso.sql"
	_change_one_pwd "SYSAUDITOR" "$sysauditor_pwd_temp" "alter_sysaud.sql"
	# 3. 执行固定巡检脚本
	local -A tasks=(
		[check_backup.sql]="备份数据库"
		[basic_dbinfo.sql]="查询数据库基础参数信息"
		[query_dmini.sql]="查询数据库优化结果"
	)
	for script in check_backup.sql basic_dbinfo.sql query_dmini.sql; do
		log_print "${tasks[$script]}"
		execute_script "SYSDBA" "${sysinfo[SYSDBA]}" "$script"
	done
	# 4. 密码提示
	log_print "请妥善保管密码，遗失无法找回！"
	for u in SYSDBA SYSSSO SYSAUDITOR; do
		[[ ${sysinfo[$u]} ]] && printf "用户 [%s] 的密码是: \033[1;31m%s\033[0m\n\n" "$u" "${sysinfo[$u]}"
	done
	# 5. 日志备份
	"$CP_CMD" -prf "$dminstalllog" "$env_dm_home/log" 2>/dev/null
	"$CP_CMD" -prf "$js_data" "$env_dm_home/log/${js_data}${current}" 2>/dev/null
}
#========================================================
# 函数：user_ssh_trust
# 作用：为当前用户在所有目标节点之间建立 SSH 互信
#========================================================
function user_ssh_trust() {
	# 1：获取当前用户名
	# shellcheck disable=SC2155
	local user=$(whoami)
	# 2：检查是否已存在互信
	log_print "配置 $user 用户互信"
	if [[ $(ssh_check "$user" "${all_pubip_arr[@]}") == "true" ]]; then
		color_printf blue "$user 用户已经互信，无需再次配置。"
		return
	fi
	# 3：关闭 SSH 主机密钥检查
	local ssh_config_file="/etc/ssh/ssh_config"
	local config_line="StrictHostKeyChecking no"
	if ! grep -q "^$config_line$" "$ssh_config_file"; then
		run_cmd printf "%s\n" "$config_line" | run_cmd tee -a "$ssh_config_file" >/dev/null
	fi
	# 4：最多重试 3 次完成互信
	local max_attempts=3
	local attempt=1
	while [[ $(ssh_check "$user" "${all_pubip_arr[@]}") == "false" && attempt -lt max_attempts ]]; do
		ssh_trust "$user" "$root_passwd" "$attempt" "${all_pubip_arr[@]}"
		((attempt++))
	done
	# 5：互信失败则退出
	if [[ $(ssh_check "$user" "${all_pubip_arr[@]}") == "false" ]]; then
		printf "\n"
		color_printf red "$user 用户互信失败，请检查参数 [-rp] 传入的 $user 密码：$root_passwd 是否正确！"
		printf "\n"
		exit 1
	fi
}
#========================================================
# 函数：send_node_scripts
# 作用：将脚本及安装包分发到所有非首节点
#========================================================
function send_node_scripts() {
	# 1：判断是否使用 SFTP
	log_print "拷贝脚本以及安装包到部署节点"
	if [[ -n "$sftp_server_ip" && -n "$sftp_server_port" && -n "$sftp_username" && -n "$sftp_passwd" && -n "$sftp_dir" ]]; then
		# 2：SFTP 模式——先上传再下载
		color_printf white "上传脚本以及安装包到sftp服务器：$sftp_server_ip"
		printf "\n"
		sftp_cmd "$sftp_server_ip" "$sftp_server_port" "$sftp_username" "$sftp_passwd" "$sftp_dir" "$software_dir" "UP"
		export -f sftp_cmd
		# 3：各节点通过 SFTP 下载
		for ((x = 1; x < ${#all_pubip_arr[@]}; x++)); do
			ip="${all_pubip_arr[$x]}"
			color_printf white "下载脚本以及安装包到节点：$ip"
			printf "\n"
			recreate_dir "$ip"
			exec_ssh_cmd "$ip" "$(declare -f sftp_cmd) sftp_cmd \"${sftp_server_ip}\" \"$sftp_server_port\" \"${sftp_username}\" \"${sftp_passwd}\" \"${sftp_dir}\" \"${software_dir}\" \"DOWN\""
		done
	else
		# 4：SCP 模式——逐节点拷贝
		for ((x = 1; x < ${#all_pubip_arr[@]}; x++)); do
			ip="${all_pubip_arr[$x]}"
			color_printf white "拷贝脚本以及安装包到节点：$ip"
			printf "\n"
			recreate_dir "$ip"
			# 5：发送安装包
			if [[ -n "${dm_iso_name}" ]]; then
				exec_scp_cmd "${software_dir}/${dm_iso_name}" "$ip" "${software_dir}" "UP"
			else
				exec_scp_cmd "${software_dir}/DMInstall.bin" "$ip" "${software_dir}" "UP"
			fi
			# 6：发送脚本
			exec_scp_cmd "${software_dir}/$(basename "$0")" "$ip" "${software_dir}" "UP"
		done
	fi
}
#========================================================
# 函数：install_other_nodes
# 作用：在所有非首节点远程执行安装脚本
#========================================================
function install_other_nodes() {
	local exec_cmd
	# 1：遍历所有非首节点
	for ((x = 1; x < ${#all_pubip_arr[@]}; x++)); do
		ip="${all_pubip_arr[$x]}"
		log_print "开始配置节点: $ip"
		color_printf yellow "节点 $ip 开始配置:"
		# 2：判断是否为监控节点，设置 node_num_seq
		if [[ "$ip" == "$dw_monitor_ip" ]]; then
			node_num_seq=-1
		else
			node_num_seq=$((x + 1))
		fi
		# 3：构造远程执行命令（自动补全 -sp 参数）
		if [[ "$node_cmd" =~ -sp ]]; then
			exec_cmd="$node_cmd"
		else
			exec_cmd="$node_cmd -sp '${sysdba_pwd_temp}'"
		fi
		# 4：远程执行脚本
		exec_ssh_cmd "$ip" "cd ${software_dir} && eval \"$exec_cmd -oguid ${oguid} -dim ${dm_install_mode} -dbv ${db_version} -node ${node_num_seq}\""
		# 5：合并各节点生成的 js 数据
		exec_ssh_cmd "$ip" "cat ${software_dir}/${js_data}" >>"${software_dir}/${js_data}"
		# 6：打印完成提示
		printf "\n"
		color_printf red "节点 $ip 配置完成."
	done
}
#========================================================
# 函数：restart_hosts
# 作用：统一关闭数据库并重启主机
#========================================================
function restart_hosts() {
	# 1：定义工具函数
	_stop_db() {
		local host=$1 cmd=$2
		printf "\n"
		color_printf red "正在关闭 $host 节点数据库服务..."
		printf "\n"
		exec_ssh_cmd "$host" "su - dmdba -c '$cmd'"
	}
	_reboot_node() {
		local host=$1
		exec_ssh_cmd "$host" "shutdown -r now &"
	}
	_prompt() {
		local msg=$1 ans
		read -rp $'\033[1;34m'"$msg [Y/N] "$'\E[0m' -e ans
		case $ans in
		[Yy]) return 0 ;;
		[Nn] | "")
			printf "\n"
			color_printf yellow "操作已取消，请手动重启"
			printf "\n"
			exit 1
			;;
		*) _prompt "$msg" ;;
		esac
	}
	# 2：根据部署模式统一重启
	case $dm_install_mode in
	single)
		_prompt "恭喜！单机安装成功，现在是否继续关闭数据库并重启主机？"
		if [[ $install_until_db =~ [Nn] && $only_conf_os =~ [Nn] ]]; then
			printf "\n"
			color_printf red "正在关闭 $(hostname -I | awk '{print $1}') 节点数据库服务..."
			printf "\n"
			run_cmd su - dmdba -c "$DMS_STOP_CMD"
		fi
		printf "\n"
		shutdown -r now &
		;;
	datawatch)
		_prompt "恭喜！ 数据守护集群安装成功，现在是否继续关闭数据库并重启主机？"
		[[ -n $dw_monitor_ip ]] && {
			_stop_db "$dw_monitor_ip" "$DMM_STOP_CMD"
			_reboot_node "$dw_monitor_ip"
		}
		for ((x = 0; x < ${#all_malip_arr[@]}; x++)); do
			ip="${all_pubip_arr[x]}"
			_stop_db "${ip}" "$DMW_STOP_CMD && $DMS_STOP_CMD"
			if ((x > 0)); then
				_reboot_node "${ip}"
				printf "\n"
			fi
		done
		# 当前节点最后重启
		shutdown -r now &
		printf "\n"
		;;
	raft)
		_prompt "恭喜！ raft 集群安装成功，现在是否继续关闭数据库并重启主机？"
		for ((i = ${#all_malip_arr[@]} - 1; i >= 0; i--)); do
			ip=${all_pubip_arr[i]}
			_stop_db "$ip" "$DMS_STOP_CMD"
			_reboot_node "$ip"
		done
		;;
	dsc | dscm)
		_prompt "恭喜！DSC 集群安装成功，现在是否继续关闭数据库并重启主机？"
		printf "\n"
		color_printf red "正在关闭 DB 实例..."
		printf "\n"
		su - dmdba -c "$env_dm_home/bin/dmcssm $data_dir/$dsc_db_name/dmcssm.ini" <<EOF
ep stop GRP_${dsc_db_name}
exit
EOF
		color_printf red "正在关闭 ASM 实例..."
		printf "\n"
		su - dmdba -c "$env_dm_home/bin/dmcssm $data_dir/$dsc_db_name/dmcssm.ini" <<EOF
ep stop GRP_ASM
exit
EOF
		for ((i = ${#all_malip_arr[@]} - 1; i >= 0; i--)); do
			ip=${all_pubip_arr[i]}
			_stop_db "$ip" "$DMC_STOP_CMD"
			_reboot_node "$ip"
		done
		;;
	esac
}
#========================================================
# 函数：parse_args
# 作用：解析脚本接收的参数
#========================================================
function parse_args() {
	# 1：循环解析所有传入参数
	while [[ $1 ]]; do
		case $1 in
		# 2：节点序号
		-node | --node_num)
			node_num=$2
			shift 2
			;;
		# 3：是否使用 ISO 镜像
		-iso | --iso)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			iso=$2
			shift 2
			;;
		# 4：主机名
		-hn | --hostname)
			checkpara_null "$1" "$2"
			hostname=$2
			shift 2
			;;
		# 5：root 密码
		-rp | --root_passwd)
			checkpara_null "$1" "$2"
			root_passwd=$2
			shift 2
			;;
		# 6：SSH 私钥路径
		-skp | --ssh_key_path)
			checkpara_null "$1" "$2"
			ssh_key_path=$2
			shift 2
			;;
		# 7：SFTP 用户名
		-sfu | --sftp_username)
			checkpara_null "$1" "$2"
			sftp_username=$2
			shift 2
			;;
		# 8：SFTP 密码
		-sfp | --sftp_passwd)
			checkpara_null "$1" "$2"
			sftp_passwd=$2
			shift 2
			;;
		# 9：达梦安装包密码
		-dp | --dm_passwd)
			checkpara_null "$1" "$2"
			dm_passwd=$2
			gen_password "$dm_passwd" -dp
			shift 2
			;;
		# 10：数据库公网 IP
		-dpi | --db_public_ip)
			checkpara_null "$1" "$2"
			db_public_ip=$2
			while IFS= read -r ip; do
				db_pubip_arr+=("-dpi:$ip")
			done < <(tr ',' '\n' <<<"${db_public_ip}")
			shift 2
			;;
		# 11：数据库 MAL IP
		-dmi | --db_mal_ip)
			checkpara_null "$1" "$2"
			db_mal_ip=$2
			while IFS= read -r ip; do
				db_malip_arr+=("-dmi:$ip")
			done < <(tr ',' '\n' <<<"${db_mal_ip}")
			shift 2
			;;
		# 12：AS 公网 IP
		-api | --as_public_ip)
			checkpara_null "$1" "$2"
			as_public_ip=$2
			while IFS= read -r ip; do
				as_pubip_arr+=("-api:$ip")
			done < <(tr ',' '\n' <<<"${as_public_ip}")
			shift 2
			;;
		# 13：AS MAL IP
		-ami | --as_mal_ip)
			checkpara_null "$1" "$2"
			as_mal_ip=$2
			while IFS= read -r ip; do
				as_malip_arr+=("-ami:$ip")
			done < <(tr ',' '\n' <<<"${as_mal_ip}")
			shift 2
			;;
		# 14：SY 公网 IP
		-spi | --sy_public_ip)
			checkpara_null "$1" "$2"
			sy_public_ip=$2
			while IFS= read -r ip; do
				sy_pubip_arr+=("-spi:$ip")
			done < <(tr ',' '\n' <<<"${sy_public_ip}")
			shift 2
			;;
		# 15：SY MAL IP
		-smi | --sy_mal_ip)
			checkpara_null "$1" "$2"
			sy_mal_ip=$2
			while IFS= read -r ip; do
				sy_malip_arr+=("-smi:$ip")
			done < <(tr ',' '\n' <<<"${sy_mal_ip}")
			shift 2
			;;
		# 16：数据守护监视器 IP
		-dmoi | --dw_monitor_ip)
			checkpara_null "$1" "$2"
			dw_monitor_ip=$2
			shift 2
			;;
		# 17：NTP 服务器 IP
		-ti | --timeserver_ip)
			checkpara_null "$1" "$2"
			timeserver_ip=$2
			check_ip "-ti:${timeserver_ip}"
			shift 2
			;;
		# 18：链路检测 IP
		-lci | --link_check_ip)
			checkpara_null "$1" "$2"
			link_check_ip=$2
			check_ip "-lci:${link_check_ip}"
			shift 2
			;;
		# 19：是否必须 ping 通
		-inp | --is_nec_ping)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			is_nec_ping=$2
			shift 2
			;;
		# 20：dminit 额外参数
		-dpv | --dminit_para_value)
			checkpara_null "$1" "$2"
			dminit_para_value=$2
			shift 2
			;;
		# 21：SSH 端口
		-osp | --os_ssh_port)
			checkpara_null "$1" "$2"
			os_ssh_port=$2
			shift 2
			;;
		# 22：SFTP 服务器 IP
		-sfi | --sftp_server_ip)
			checkpara_null "$1" "$2"
			sftp_server_ip=$2
			check_ip "-sfi:{$sftp_server_ip}"
			shift 2
			;;
		# 23：SFTP 服务器端口
		-sfo | --sftp_server_port)
			checkpara_null "$1" "$2"
			sftp_server_port=$2
			shift 2
			;;
		# 24：达梦安装目录
		-d | --env_dm_home)
			checkpara_null "$1" "$2"
			env_dm_home=${2%/}
			shift 2
			;;
		# 25：归档目录
		-ad | --arch_dir)
			checkpara_null "$1" "$2"
			arch_dir=${2%/}
			shift 2
			;;
		# 26：备份目录
		-bd | --backup_dir)
			checkpara_null "$1" "$2"
			backup_dir=${2%/}
			shift 2
			;;
		# 27：core 目录
		-cd | -core_dir)
			checkpara_null "$1" "$2"
			core_dir=${2%/}
			shift 2
			;;
		# 28：数据目录
		-dd | --data_dir)
			checkpara_null "$1" "$2"
			data_dir=${2%/}
			shift 2
			;;
		# 29：SFTP 目录
		-sfd | --sftp_dir)
			checkpara_null "$1" "$2"
			sftp_dir=${2%/}
			shift 2
			;;
		# 30：数据库名
		-dn | --db_name)
			checkpara_null "$1" "$2"
			db_name=$2
			shift 2
			;;
		# 31：实例名
		-in | --instance_name)
			checkpara_null "$1" "$2"
			instance_name=$2
			shift 2
			;;
		# 32：extent 大小
		-es | --extent_size)
			checkpara_null "$1" "$2"
			extent_size=$2
			checkpara_validity "$1" "$extent_size" "16" "32" "64"
			shift 2
			;;
		# 33：页大小
		-ps | --page_size)
			checkpara_null "$1" "$2"
			page_size=$2
			checkpara_validity "$1" "$page_size" "4" "8" "16" "32"
			shift 2
			;;
		# 34：大小写敏感
		-cs | --case_sensitive)
			checkpara_null "$1" "$2"
			case_sensitive=$2
			checkpara_validity "$1" "$case_sensitive" "Y" "N" "1" "0"
			shift 2
			;;
		# 35：字符集
		-c | --charset)
			checkpara_null "$1" "$2"
			charset=$2
			checkpara_validity "$1" "$charset" "0" "1" "2"
			shift 2
			;;
		# 36：字符长度单位
		-lic | --length_in_char)
			checkpara_null "$1" "$2"
			length_in_char=$2
			checkpara_validity "$1" "$length_in_char" "0" "1" "Y" "N"
			shift 2
			;;
		# 37：兼容模式
		-cm | --compatible_mode)
			checkpara_null "$1" "$2"
			compatible_mode=$2
			checkpara_validity "$1" "$compatible_mode" "0" "1" "2" "3" "4" "5" "6" "7"
			shift 2
			;;
		# 38：空格填充模式
		-bpm | --blank_pad_mode)
			checkpara_null "$1" "$2"
			blank_pad_mode=$2
			checkpara_validity "$1" "$blank_pad_mode" "0" "1"
			shift 2
			;;
		# 39：本地 OS 认证
		-osa | --os_auth)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			os_auth=$2
			shift 2
			;;
		# 40：日志文件大小
		-ls | --log_size)
			checkpara_null "$1" "$2"
			log_size=$2
			validate_number "$1" "$log_size" "INT_R" "256" "4096"
			shift 2
			;;
		# 41：redo 文件名后缀
		-rpn | --rlog_postfix_name)
			checkpara_null "$1" "$2"
			validate_str "$1" "$2"
			rlog_postfix_name=$2
			shift 2
			;;
		# 42：是否启用归档
		-er | --enable_arch)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			enable_arch=$2
			shift 2
			;;
		# 43：空间限制
		-sl | --space_limit)
			checkpara_null "$1" "$2"
			space_limit=$2
			validate_number "$1" "$space_limit" "INT_0" "10240"
			shift 2
			;;
		# 44：端口号
		-pn | --port_num)
			checkpara_null "$1" "$2"
			port_num=$2
			shift 2
			;;
		# 45：SYSDBA 密码
		-sp | --sysdba_pwd)
			checkpara_null "$1" "$2"
			sysdba_pwd=$2
			[[ $sysdba_pwd != "Dm@SYSDBA123" ]] && gen_password "$sysdba_pwd" -sp
			shift 2
			;;
		# 46：SYSAUDITOR 密码
		-sap | --sysaud_pwd)
			checkpara_null "$1" "$2"
			sysaud_pwd=$2
			[[ $sysaud_pwd != "Dm@SYSAUD123" ]] && gen_password "$sysaud_pwd" -sap
			shift 2
			;;
		# 47：SYSSSO 密码
		-ssp | --syssso_pwd)
			checkpara_null "$1" "$2"
			syssso_pwd=$2
			[[ $syssso_pwd != "Dm@SYSSSO123" ]] && gen_password "$syssso_pwd" -ssp
			shift 2
			;;
		# 48：备份模式
		-bm | --backup_mode)
			checkpara_null "$1" "$2"
			backup_mode=$2
			checkpara_validity "$1" "$backup_mode" "1" "2"
			shift 2
			;;
		# 49：仅配置 OS
		-m | --only_conf_os)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			only_conf_os=$2
			shift 2
			;;
		# 50：仅安装到数据库
		-ud | --install_until_db)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			install_until_db=$2
			shift 2
			;;
		# 51：仅初始化数据库
		-oid | --only_init_db)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			only_init_db=$2
			shift 2
			;;
		# 52：是否优化数据库
		-opd | --optimize_db)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			optimize_db=$2
			shift 2
			;;
		# 53：内存百分比
		-mp | --mem_percent)
			checkpara_null "$1" "$2"
			mem_percent=$2
			shift 2
			;;
		# 54：OLTP 模式
		-om | --oltp_mode)
			checkpara_null "$1" "$2"
			oltp_mode=$2
			shift 2
			;;
		# 55：ISO 文件名
		-di | --dm_iso_name)
			checkpara_null "$1" "$2"
			dm_iso_name=$2
			shift 2
			;;
		# 56：key 文件路径
		-kp | --key_path)
			checkpara_null "$1" "$2"
			key_path=$2
			shift 2
			;;
		# 57：守护组名
		-dgn | --dw_group_name)
			checkpara_null "$1" "$2"
			dw_group_name=$2
			shift 2
			;;
		# 58：MAL 端口
		-mpn | --mal_port_num)
			checkpara_null "$1" "$2"
			mal_port_num=$2
			shift 2
			;;
		# 59：MAL DW 端口
		-mdpn | --mal_dw_port_num)
			checkpara_null "$1" "$2"
			mal_dw_port_num=$2
			shift 2
			;;
		# 60：MAL 实例 DW 端口
		-midpn | --mal_inst_dw_port_num)
			checkpara_null "$1" "$2"
			mal_inst_dw_port_num=$2
			shift 2
			;;
		# 61：OGUID
		-oguid | --oguid)
			checkpara_null "$1" "$2"
			oguid=$2
			validate_number "$1" "$oguid" "INT_6"
			shift 2
			;;
		# 62：安装模式
		-dim | --dm_install_mode)
			dm_install_mode=$2
			shift 2
			;;
		# 63：归档类型
		-at | --arch_type)
			checkpara_null "$1" "$2"
			arch_type=$2
			checkpara_validity "$1" "$arch_type" "0" "1"
			shift 2
			;;
		# 64：重复间隔
		-ri | --repeat_interval)
			checkpara_null "$1" "$2"
			repeat_interval=$2
			validate_number "$1" "$repeat_interval" "INT_0"
			shift 2
			;;
		# 65：归档恢复时间
		-art | --arch_recover_time)
			checkpara_null "$1" "$2"
			arch_recover_time=$2
			validate_number "$1" "$arch_recover_time" "INT_0"
			shift 2
			;;
		# 66：归档等待应用
		-awa | --arch_wait_apply)
			checkpara_null "$1" "$2"
			arch_wait_apply=$2
			checkpara_validity "$1" "$arch_wait_apply" "0" "1"
			shift 2
			;;
		# 67：DSC 数据库名
		-ddn | --dsc_db_name)
			checkpara_null "$1" "$2"
			dsc_db_name=$2
			shift 2
			;;
		# 68：DSC 实例名前缀
		-den | --dsc_ep_name)
			checkpara_null "$1" "$2"
			dsc_ep_name=$2
			shift 2
			;;
		# 69：DCR 磁盘
		-dcd | --dcr_disk)
			checkpara_null "$1" "$2"
			dcr_disk=$2
			shift 2
			;;
		# 70：VOTE 磁盘
		-vod | --vote_disk)
			checkpara_null "$1" "$2"
			vote_disk=$2
			shift 2
			;;
		# 71：LOG 磁盘
		-lod | --log_disk)
			checkpara_null "$1" "$2"
			log_disk=$2
			shift 2
			;;
		# 72：归档磁盘
		-ard | --arch_disk)
			checkpara_null "$1" "$2"
			arch_disk=$2
			shift 2
			;;
		# 73：数据磁盘
		-dad | --data_disk)
			checkpara_null "$1" "$2"
			data_disk=$2
			shift 2
			;;
		# 74：AU 大小
		-as | --au_size)
			checkpara_null "$1" "$2"
			au_size=$2
			checkpara_validity "$1" "$au_size" "1" "2" "4" "8" "16" "32" "64"
			shift 2
			;;
		# 75：redo 大小
		-rs | redo_size)
			checkpara_null "$1" "$2"
			redo_size=$2
			validate_number "$1" "$redo_size" "INT_R" "64" "2048"
			shift 2
			;;
		# 76：redo 冗余
		-rr | --redo_redun)
			checkpara_null "$1" "$2"
			redo_redun=$2
			checkpara_validity "$1" "$redo_redun" "1" "2" "3"
			shift 2
			;;
		# 77：归档冗余
		-ar | --arch_redun)
			checkpara_null "$1" "$2"
			arch_redun=$2
			checkpara_validity "$1" "$redo_redun" "1" "2" "3"
			shift 2
			;;
		# 78：数据冗余
		-dr | --data_redun)
			checkpara_null "$1" "$2"
			data_redun=$2
			checkpara_validity "$1" "$redo_redun" "1" "2" "3"
			shift 2
			;;
		# 79：控制文件镜像
		-ctm | --ctl_mirror)
			checkpara_null "$1" "$2"
			ctl_mirror=$2
			checkpara_validity "$1" "$ctl_mirror" "1" "2" "3"
			shift 2
			;;
		# 80：数据镜像
		-dtm | --data_mirror)
			checkpara_null "$1" "$2"
			data_mirror=$2
			checkpara_validity "$1" "$data_mirror" "1" "2" "3"
			shift 2
			;;
		# 81：日志镜像
		-lgm | --log_mirror)
			checkpara_null "$1" "$2"
			log_mirror=$2
			checkpara_validity "$1" "$log_mirror" "1" "2" "3"
			shift 2
			;;
		# 82：归档 ASM 镜像
		-aam | --arch_asm_mirror)
			checkpara_null "$1" "$2"
			arch_asm_mirror=$2
			checkpara_validity "$1" "$arch_asm_mirror" "1" "2" "3"
			shift 2
			;;
		# 83：数据条带化
		-dst | --data_striping)
			checkpara_null "$1" "$2"
			data_striping=$2
			checkpara_validity "$1" "$data_striping" "0" "32" "64" "128" "256"
			shift 2
			;;
		# 84：日志条带化
		-lst | --log_striping)
			checkpara_null "$1" "$2"
			log_striping=$2
			checkpara_validity "$1" "$log_striping" "0" "32" "64" "128" "256"
			shift 2
			;;
		# 85：归档 ASM 条带化
		-aas | --arch_asm_striping)
			checkpara_null "$1" "$2"
			arch_asm_striping=$2
			checkpara_validity "$1" "$arch_asm_striping" "0" "32" "64" "128" "256"
			shift 2
			;;
		# 86：是否本地归档
		-ila | --is_local_arch)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			is_local_arch=$2
			shift 2
			;;
		# 87：ASM 密码
		-apd | --asm_pwd)
			checkpara_null "$1" "$2"
			asm_pwd=$2
			shift 2
			;;
		# 88：CSS DCR EP 端口
		-cdp | --css_dcr_ep_port)
			checkpara_null "$1" "$2"
			css_dcr_ep_port=$2
			shift 2
			;;
		# 89：ASM DCR EP 端口
		-adp | --asm_dcr_ep_port)
			checkpara_null "$1" "$2"
			asm_dcr_ep_port=$2
			shift 2
			;;
		# 90：DB DCR EP 端口
		-ddp | --db_dcr_ep_port)
			checkpara_null "$1" "$2"
			db_dcr_ep_port=$2
			shift 2
			;;
		# 91：ASVR MAL 端口
		-amp | --asvrmal_mal_port)
			checkpara_null "$1" "$2"
			asvrmal_mal_port=$2
			shift 2
			;;
		# 92：DB MAL 端口
		-dmp | --db_mal_port)
			checkpara_null "$1" "$2"
			db_mal_port=$2
			shift 2
			;;
		# 93：多路径
		-mtp | --multipath)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			multipath=$2
			shift 2
			;;
		# 94：别名替换 IP
		-ari | --alias_replaces_ip)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			alias_replaces_ip=$2
			shift 2
			;;
		# 95：仅创建用户
		-ocu | --only_create_user)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			only_create_user=$2
			create_os_user
			shift 2
			exit 1
			;;
		# 96：仅安装 sshpass
		-ois | --only_install_sshpass)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			only_install_sshpass=$2
			shift 2
			;;
		# 97：是否使用 sshpass
		-isp | --is_sshpass)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			is_sshpass=$2
			shift 2
			;;
		# 98：Raft 心跳间隔
		-rhi | --raft_hb_interval)
			checkpara_null "$1" "$2"
			raft_hb_interval=$2
			shift 2
			;;
		# 99：Raft 投票间隔
		-rvi | --raft_vote_interval)
			checkpara_null "$1" "$2"
			raft_vote_interval=$2
			shift 2
			;;
		# 100：XMAL 心跳间隔
		-xhi | --xmal_hb_interval)
			checkpara_null "$1" "$2"
			xmal_hb_interval=$2
			shift 2
			;;
		# 101：XMAL IP
		-xi | --xmal_ip)
			checkpara_null "$1" "$2"
			xmal_ip=$2
			while IFS= read -r ip; do
				xmal_ip_arr+=("-xi:$ip")
			done < <(tr ',' '\n' <<<"${xmal_ip}")
			shift 2
			;;
		# 102：XMAL 端口
		-xp | --xmal_port)
			checkpara_null "$1" "$2"
			xmal_port=$2
			shift 2
			;;
		# 103：归档目标端口
		-adpt | --arch_dest_port)
			checkpara_null "$1" "$2"
			arch_dest_port=$2
			shift 2
			;;
		# 104：数据库版本
		-dbv | --db_version)
			checkpara_null "$1" "$2"
			db_version=$2
			shift 2
			;;
		# 105：调试开关
		-dbg | --debug_flag)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			debug_flag=$2
			shift 2
			;;
		# 106：过滤磁盘
		-fld | --fil_disk)
			checkpara_null "$1" "$2"
			fil_disk=$2
			filter_disk "$fil_disk"
			shift 2
			exit 1
			;;
		# 107：是否格式化磁盘
		-fmd | --format_disk)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			format_disk=$2
			shift 2
			;;
		# 108：是否配置大页
		-hf | --huge_flag)
			checkpara_null "$1" "$2"
			checkpara_yn "$1" "$2"
			huge_flag=$2
			shift 2
			;;
		# 109：帮助信息
		-h | --help) help ;;
		-hh | --hidden_help) hidden_help ;;
		# 110：未知参数处理
		*)
			printf "\n"
			color_printf red "脚本已退出安装，请检查参数 '$1 $2' 是否正确，可以查看脚本帮助：'sh $(basename "$0") --help' 获取更多信息！"
			printf "\n"
			exit 1
			;;
		esac
	done
}
#========================================================
# 函数：install_mode
# 作用：在首节点交互式选择数据库安装模式与版本
#========================================================
function install_mode() {
	# 1：仅首节点执行
	[[ $node_num -ne 1 ]] && return
	# 2：定义映射表
	local -A mode_map=(
		["1"]="single"
		["2"]="datawatch"
		["3"]="raft"
		["4"]="dsc"
		["5"]="dscm"
	)
	local dbversion="8|9"
	# 3：若未指定模式，则交互选择
	if [[ -z $dm_install_mode ]]; then
		while :; do
			printf "\n"
			read -rep "$(echo -e "\033[34m请选择达梦数据库部署类型：单机[1]-数据守护[2]-raft主备[3]-dsc集群[4]-dsc集群(ASM镜像)[5]: \E[0m")" dm_install_mode
			printf "\n"
			if [[ -n "$dm_install_mode" && ${mode_map[$dm_install_mode]+exists} ]]; then
				dm_install_mode=${mode_map[$dm_install_mode]}
				color_printf green "达梦数据库安装部署类型： $dm_install_mode"
				break
			else
				color_printf red "无效输入！请选择 1、2、3、4 或 5！"
			fi
		done
		while :; do
			echo
			read -rep "$(echo -e "\033[34m请选择数据库版本 [$dbversion]（退出安装输入 0） : \E[0m")" db_version
			echo
			if [[ "$db_version" =~ ^($dbversion)$ ]]; then
				color_printf green "数据库版本：" "$db_version"
				break
			elif [[ $db_version -eq 0 ]]; then
				color_printf red "退出安装"
				printf "\n"
				exit 1
			else
				color_printf yellow "无效输入！请选择 8 或 9"
			fi
		done
	else
		# 4：已指定模式，仅校验
		if [[ -n "$dm_install_mode" && ${mode_map[$dm_install_mode]+exists} ]]; then
			printf "\n"
			dm_install_mode=${mode_map[$dm_install_mode]}
			color_printf green "达梦数据库安装部署类型： $dm_install_mode"
		else
			printf "\n"
			color_printf red "达梦数据库部署类型选择错误，请输入 1 或 2 或 3 或 4 或 5！"
			printf "\n"
			exit 1
		fi
		if [[ "$db_version" =~ ^($dbversion)$ ]]; then
			color_printf green "数据库版本：" "$db_version"
		else
			color_printf yellow "无效输入！请选择 8 或 9"
			exit 1
		fi
	fi
}
#========================================================
# 函数：conf_pwq_temp
# 逻辑：关闭登录提示,临时关闭密码复杂度策略
#========================================================
function conf_pwq_temp() {
	export pwq_conf=/etc/security/pwquality.conf
	local or_file=/etc/os-release
	local su_file=/etc/pam.d/su
	local os_id
	# 关闭 last-login 提示
	run_cmd sed -i 's/^\(session.*postlogin\)/# \1/' "$su_file"
	# 备份pwquality.conf文件
	bak_file "$pwq_conf"
	# 获取 OS ID
	[[ -f "$or_file" ]] && os_id=$(awk -F= '/^ID=/ {gsub(/"/,"",$2); print $2}' "$or_file") || return
	[[ ${os_id,,} =~ (uos|kylin|openeuler) ]] && {
		# shellcheck disable=SC2016
		run_cmd sed -i '$a enforcing = 0\nminlen = 6\nminclass = 0' "$pwq_conf"
	}
}
#========================================================
# 函数：redefine_para
# 参数：根据传参和架构，重定义参数，使其符合数据库部署要求
#========================================================
function redefine_para() {
	# 重定义core_dir目录
	[[ -z "$core_dir" ]] && core_dir=${backup_dir}/dmcore
	# 当空格填充模式等于1时，必定时兼容Oracle
	[[ ${blank_pad_mode} -eq 1 ]] && compatible_mode=2
	# 数据守护/raft 集群动态生成 instance_name
	local prefix key_file
	if [[ "$dm_install_mode" =~ ^(datawatch|raft)$ ]]; then
		case "$dm_install_mode" in
		"datawatch")
			# 如果确认监视器存在，即是自动切换模式
			[[ -n $dw_monitor_ip ]] && dw_mode=AUTO
			# 转换归档类型
			case $arch_type in
			0) dw_arch_type=REALTIME ;;
			1) dw_arch_type=TIMELY ;;
			esac
			# 定义数据库实例名前缀
			prefix="${dw_group_name}_DW_"
			;;
		"raft")
			prefix="EP"
			;;
		esac
		[[ "$instance_name" != "DMSERVER" ]] && prefix="$instance_name"
		instance_name="${prefix}$(printf "%02d" "$node_num")"
	fi
	# 配置授权文件
	key_file="${software_dir}/dm${oguid}.key"
	[[ -n "$key_path" && -f "$key_path" ]] && "$CP_CMD" -prf "$key_path" "$key_file"
	# 判断key文件是否存在，判断是否退出函数
	if ((node_num == 1)) || [[ -z $key_path ]] || [[ -f $key_file ]]; then
		return 0
	fi
	# 如果本机没有，则拷贝服务器
	exec_scp_cmd "$key_file" "${all_pubip_arr[0]}" "$key_file" "DOWN"
}
#========================================================
# 函数：pkg_install
# 作用：安装必要的工具
#========================================================
function pkg_install() {
	# YUM配置检查
	if [[ $iso == "Y" ]]; then
		# 合并所有需要YUM配置的条件
		if ((node_num == 1)) && [[ $dm_install_mode =~ ^(datawatch|raft|dsc|dscm)$ ]] || [[ -n $sftp_server_ip ]] || [[ $multipath == "Y" && ${disk_arr[0]:0:4} == "/dev" ]] || [[ -n $timeserver_ip ]]; then
			conf_yum
		fi
	fi
	local packages=()
	# 辅助函数：添加包到列表（如果命令不存在）
	_add_packages() {
		local check_cmd="$1"
		shift
		if ! command -v "$check_cmd" >/dev/null 2>&1; then
			packages+=("$@")
		fi
	}
	# SSH相关软件包检查
	local need_expect=false
	if ((node_num == 1)) && [[ $dm_install_mode =~ ^(datawatch|raft|dsc|dscm)$ ]] || [[ -n $sftp_server_ip ]]; then
		need_expect=true
	fi
	[[ $need_expect == true ]] && _add_packages "expect" expect openssh-clients openssh-server
	# 多路径软件包检查
	[[ $multipath == "Y" && ${disk_arr[0]:0:4} == "/dev" ]] && _add_packages "multipath" device-mapper-multipath
	# 时间同步软件包检查
	[[ -n $timeserver_ip ]] && _add_packages "ntpq" ntp chrony
	# 批量安装
	[[ ${#packages[@]} -gt 0 ]] && install_package "${packages[@]}"
}
#========================================================
# 函数：generate_json_data1
# 作用：生成 HTML 报告所需 JSON 数据的开头部分
#========================================================
function generate_json_data1() {
	# 1：仅首节点生成
	[[ $node_num -eq 1 ]] || return
	local host_ip
	host_ip=$([[ $dm_install_mode == "single" ]] && hostname -I | awk '{print $1}' || echo "${all_pubip_arr[*]}")

	# 2：写入 JSON 头
	json_data=$(
		cat <<EOF
const projectData = {
 "common": {
 	"userName": "武汉达梦数据库股份有限公司项目",
 	"projectName": "武汉达梦数据库股份有限公司",
 	"developer": "武汉达梦数据库股份有限公司",
 	"businessSystem": "XXX业务系统",
 	"deployDate": "$current",
 	"dmInstallMode": "$dm_install_mode"
 },
 "host": {
 	"hostIp": "$host_ip",
 	"hostUsername": "dmdba",
 	"hostPassword": "$dm_passwd"
 },
 "nodes": {
EOF
	)
	printf "%s\n" "$json_data" >"${software_dir}/${js_data}"
}
#========================================================
# 函数：print_node_info
# 作用：将当前节点的系统、数据库及配置信息以 JSON 格式追加到 js 数据文件
#========================================================
function print_node_info() {
	# 确定当前节点序号（-1 表示监视器节点）
	nodes=$([[ $node_num -eq -1 ]] && echo "${#all_pubip_arr[@]}" || echo "$node_num")
	# 拼接 JSON 头部及固定字段
	json_data=$(
		cat <<EOF
"node${nodes}": {
  "serverInfo": {
    "serverIp": "${sysinfo["server_ip"]}",
    "osVersion": "${sysinfo[os_version]}",
    "kernelVersion": "${sysinfo["kernel_version"]}",
    "hostname": "${sysinfo["hostname"]}"
  },

  "systemConfig": {
    "cpuInfoDetail": \`${sysinfo["cpu_info"]}\`,
    "memoryInfoDetail": \`${sysinfo["mem_total_h"]}\`,
    "networkInfoDetail": \`${sysinfo["net_info"]}\`,
    "kernelParamsDetail": \`${sysinfo["kernel_params"]}\`,
    "userResourcesDetail": \`${sysinfo["user_resources"]}\`,
    "filesystemDetail": \`${sysinfo["filesystem"]}\`,
    "firewallPortDetail": \`${sysinfo["firewall_port"]}\`,
    "selinuxDetail": \`${sysinfo["selinux"]}\`,
    "scheduleDetail": \`${sysinfo["schedule"]}\`,
    "systemDetail": \`${sysinfo["system"]}\`,
    "loginResourcesDetail": \`${sysinfo["login_resources"]}\`,
    "profile": \`${sysinfo["profile"]}\`,
    "rootEnv": \`${sysinfo["root_env"]}\`,
    "dmdbaEnv": \`${sysinfo["dmdba_env"]}\`
  },

  "directories": {
    "installDir": \`${sysinfo["install_dir"]}\`,
    "instanceDir": \`${sysinfo["instance_dir"]}\`,
    "archiveDir": \`${sysinfo["archive_dir"]}\`,
    "backupDir": \`${sysinfo["backup_dir"]}\`,
  },

  "license": {
    "licenseVersion": "${sysinfo["lic_version"]}",
    "licenseSeriesNo": "${sysinfo["lic_series_no"]}",
    "licenseServerSeries": "${sysinfo["lic_server_series"]}",
    "licenseServerType": "${sysinfo["lic_server_type"]}",
    "licenseProjectName": "${sysinfo["lic_project_name"]}",
    "licenseExpire": "${sysinfo["lic_expire"]}",
    "licenseAuthorizedCustomer": "${sysinfo["lic_authorized_customer"]}",
    "licenseAuthorizedUserNumber": "${sysinfo["lic_authorized_user_number"]}",
    "licenseConcurrencyUserNumber": "${sysinfo["lic_concurrency_user_number"]}",
    "licenseCpuCount": "${sysinfo["lic_cpu_count"]}",
    "licenseCpuCores": "${sysinfo["lic_cpu_cores"]}",
    "licenseClusterType": "${sysinfo["lic_cluster_type"]}"
  },

  "databaseInstance": {
    "dbVersion": "${sysinfo["db_ver"]}",
    "dbBuildVersion": "${sysinfo["db_build_version"]}",
    "dbName": "${db_name}",
    "dbInstance": "${instance_name}",
    "dbExtentSize":"${extent_size}",
    "dbPageSize": "${page_size}",
    "dbCaseSensitive": "${sysinfo["db_case_sensitive"]}",
    "dbCharset": "${sysinfo["db_charset"]}",
    "dbCharUnit": "${sysinfo["db_char_unit"]}",
    "dbBlankPadding": "${blank_pad_mode}",
    "dbCompatibility": "${compatible_mode}",
    "dbLogCount": "2",
    "dbLogSize": "${log_size}",
    "dbArchiveEnabled": "${enable_arch}",
    "dbCreateTime": "${sysinfo["db_create_time"]}",
    "dbStartupTime": "${sysinfo["db_startup_time"]}"
  },

  "configFiles": {
EOF
	)
	# 定义需要动态判断的 ini 文件列表
	ini_files=(
		"dmIni"
		"dmmalIni"
		"dmarchIni"
		"dmtimerIni"
		"dmwatcherIni"
		"dmmonitorIni"
		"sqllogIni"
		"multipathConf"
		"dmRules"
		"cfgIni"
		"dmasvmalIni"
		"dmdcrIni"
		"dmcssmIni"
	)
	# 遍历并追加存在的 ini 文件内容到 JSON
	for item in "${ini_files[@]}"; do
		if [ -n "${sysinfo["$item"]}" ]; then
			json_data+="
    \"$item\": \`${sysinfo["$item"]}\`,"
		fi
	done
	# 去掉最后一个多余逗号
	json_data="${json_data%,}"
	# 拼接 JSON 尾部（备份与模式信息）
	json_data+=$(
		cat <<EOF

  },
    "databaseBak": {
      "dmBak": \`${sysinfo["dm_bak"]}\`,
      "dmMode":\`${sysinfo["dm_mode"]}\`
  }
EOF
	)
	# 将 JSON 数据追加到 js 文件
	printf "%s\n" "$json_data" >>"${software_dir}/${js_data}"
	# 根据安装模式决定 JSON 结束符号是否带逗号
	if [[ $dm_install_mode == "single" ]]; then
		printf "\n%s\n" "}" >>"${software_dir}/${js_data}"
	else
		if [[ "$nodes" -lt "${#all_pubip_arr[@]}" ]]; then
			suffix=","
		else
			suffix=""
		fi
		printf "\n%s\n%s" "}" "$suffix" >>"${software_dir}/${js_data}"
	fi
}
#========================================================
# 函数：conf_os_env
# 作用：统一配置操作系统环境
#========================================================
function conf_os_env() {
	# 1：配置防火墙与端口
	conf_port_firewall
	# 2：关闭 SELinux
	conf_selinux
	# 3：配置交换分区
	conf_swap
	# 4：禁用透明大页-NUMA-IO调度
	conf_grub
	# 5：配置内核资源
	conf_sysctl
	# 6：配置用户资源
	conf_limits
	# 7：配置系统 nproc
	conf_system_nproc
	# 8：移除 IPC 服务
	conf_logind
	# 9：配置 NTP 服务
	[[ -n $timeserver_ip ]] && conf_timesync
	# 10：配置清除 cache
	conf_drop_cache
	# 11：配置语言环境
	conf_profile
	# 12：配置主机名
	[[ -n $hostname ]] && conf_hostname
	# 13：配置 hosts
	conf_hosts
	# 14：创建 dmdba 用户
	create_os_user
	# 15：创建安装目录
	create_env_dir
	# 16：配置用户环境变量
	conf_user_profile
	# 17：挂载 ISO
	[[ -n "${dm_iso_name}" ]] && mount_iso
	# 18：注册trap函数
	trap cleanup_on_exit EXIT
}
#========================================================
# 函数：install_soft
# 作用：安装数据库软件
#========================================================
function install_soft() {
	# 1：安装数据库软件
	install_dmdb
	# 2：配置 glogin
	conf_glogin
	# 3：创建实例目录
	create_ins_dir
	# 4：定义服务命令
	db_service_cmd
	# 5：卸载残存的服务
	uninstall_service
}
#========================================================
# 函数：init_si
# 作用：单机模式初始化数据库
#========================================================
function init_si() {
	# 1：初始化数据库
	init_database
	# 2：启动数据库
	start_database
	# 3：查询基本数据库基本信息
	query_basic_para
	# 4：创建基准 SQL
	create_baseline_sql
	# 5：创建优化 SQL
	create_optimize_sql
	# 6：配置 SQL 日志
	conf_sqllog
	# 7：优化数据库
	[[ $optimize_db == "Y" ]] && optimize_dmdb
	# 8：生成节点信息
	[[ $dm_install_mode == "single" ]] && print_node_info
}
#========================================================
# 函数：init_dw_rf
# 作用：数据守护-raft 模式初始化
#========================================================
function init_dw_rf() {
	local flag
	# 1：区分模式
	[[ $dm_install_mode == "datawatch" ]] && {
		if [[ $node_num -eq -1 ]]; then
			dw_monitor
			print_node_info
			return
		fi
		flag=1
	}
	# 2：主库-备库差异化
	[[ $node_num -eq 1 ]] && db_pri_backup || db_std_restore
	# 3：公共配置
	local -a config_functions=(
		db_dmini
		${flag:+dw_dmmal}
		db_dmarch
		${flag:+dw_timer}
		${flag:+dw_dmwatcher}
		${flag:+dw_monitor}
		db_startup
	)
	for func in "${config_functions[@]}"; do
		$func
	done
	# 4：生成节点信息
	print_node_info
	# 5：主库安装其他节点
	[[ $node_num -eq 1 ]] && install_other_nodes
}
#========================================================
# 函数：init_dsc
# 作用：DSC 模式初始化
#========================================================
function init_dsc() {
	# 1：配置 ASM 磁盘
	dsc_conf_disk_arr
	# 2：启动 udev
	dsc_start_udev
	# 3：创建 dmdcr_cfg.ini
	dsc_dmdcr_cfg
	# 4：创建 dmasvrmal.ini
	dsc_dmasvmal
	# 5：创建 dmdcr.ini
	dsc_dmdcr
	# 6：创建 dmcssm.ini
	dsc_dmcssm
	# 7：配置 dm_svc.conf
	dsc_dmsvc
	# 8：注册 CSS-ASM 服务
	dsc_dmservice
	# 9：查询基本参数
	query_basic_para
	# 10：创建基准 SQL
	create_baseline_sql
	# 11：创建优化 SQL
	create_optimize_sql
	# 12：首节点专属操作
	print_node_info
	# 13：打印节点信息
	if [[ $node_num -eq 1 ]]; then
		# 14：把安装介质同步到其它节点
		install_other_nodes
		# 15：使用 dmasmcmd 初始化 DCR/VOTE/ASM 磁盘
		dsc_dmasmcmd
		# 16：启动 CSS 集群并等待仲裁成功
		dsc_start_ca
		# 17：使用 dmasmtool 创建 ASM 磁盘组
		dsc_dmasmtool
		# 18：初始化数据库实例（建库）
		dsc_init_db
		# 19：配置 SQL 日志参数
		conf_sqllog
		# 20：启动数据库到 OPEN 状态
		dsc_startup
		# 21：如启用优化标志，则执行数据库优化脚本
		[[ $optimize_db == "Y" ]] && optimize_dmdb
	fi
}
#========================================================
# 函数：generate_json_data2
# 作用：生成数据库 HTML 报告所需 JSON 数据的结尾部分
#========================================================
function generate_json_data2() {
	# 1：仅首节点执行
	[[ $node_num -eq 1 ]] || return
	local host_ip result
	host_ip=$([[ $dm_install_mode == "single" ]] && hostname -I | awk '{print $1}' || echo "${all_pubip_arr[*]}")
	# 2：复制并处理 IP 数组
	local temp_arr=("${all_pubip_arr[@]}")
	[[ -n "$dw_monitor_ip" ]] && {
		unset 'temp_arr[${#temp_arr[@]}-1]'
		temp_arr=("${temp_arr[@]}")
	}
	# 3：拼接 IP:Port 字符串
	local result=""
	for ip in "${temp_arr[@]}"; do
		[[ -z "$result" ]] && result="${ip}:${port_num}" || result="${result},${ip}:${port_num}"
	done
	# 4：添加dsc配置文件
	_add_dsc_params() {
		if [[ $dm_install_mode =~ ^(dsc|dscm)$ ]]; then
			data+=$(
				cat <<EOF

"dscConf":{
  "asmDiskFile":\`${sysinfo[asmDiskFile]}\`,
  "dmasmtoolCmd":\`${sysinfo[dmasmtoolCmd]}\`,
  "dscDminitFile":\`${sysinfo[dscDminitFile]}\`
},
EOF
			)
			echo -e "$data"
		fi
	}

	# 5：追加 JSON 尾段
	json_data=$(
		cat <<EOF
},

$(_add_dsc_params)

"database": {
  "dbIp": "${host_ip}",
  "dbPort": "${port_num}",
  "sysdbaPassword": "${sysinfo["SYSDBA"]}",
  "sysauditorPassword": "${sysinfo["SYSAUDITOR"]}",
  "sysssoPassword": "${sysinfo["SYSSSO"]}"
},

"operations": {
  "single": {
  	"standaloneStop": "1.切换dmdba用户：su - dmdba\n2.停止数据库：DmService${db_name} stop",
  	"standaloneStart": "1.切换dmdba用户：su - dmdba\n2.启动数据库：DmService${db_name} start",
  	"standaloneManager": "${env_dm_home}/tool/manager",
  	"standaloneDisql": "disql SYSDBA/'\"${sysinfo["SYSDBA"]}\"'@127.0.0.1:${port_num}"
  },
  "datawatch": {
  	"datawatchStop": "1.切换dmdba用户：su - dmdba\n2.停止监视器：DmMonitorService${dw_group_name} stop\n3.停止守护进程：DmWatcherService${dw_group_name} stop\n4.停止数据库：DmService${db_name} stop",
  	"datawatchStart": "1.切换dmdba用户：su - dmdba\n2.启动数据库：DmService${db_name} start\n3.启动守护进程：DmWatcherService${dw_group_name} start\n4.启动监视器：DmMonitorService${dw_group_name} start",
  	"datawatchManager": "连接到主库实例进行管理",
  	"datawatchDisql": "disql SYSDBA/'\"${sysinfo["SYSDBA"]}\"'@127.0.0.1:${port_num}",
  	"datawatchNote": \`所有集群节点时间需要同步，误差在30秒内，
停止顺序- 监视器 -> 备库守护进程 -> 主库守护进程 -> 备库实例 -> 主库实例
启动顺序- 主库实例 -> 备库实例 -> 主库守护进程 -> 备库守护进程 -> 监视器\`
  },
  "raft": {
  	"raftStop": "1.切换dmdba用户：su - dmdba\n2.停止数据库：DmService${db_name} stop",
  	"raftStart": "1.切换dmdba用户：su - dmdba\n2.启动数据库：DmService${db_name} start",
  	"raftManager": "连接到主库实例进行管理",
  	"raftDisql": "disql SYSDBA/'\"${sysinfo["SYSDBA"]}\"'@127.0.0.1:${port_num}",
  	"raftNote": \`1.所有集群节点时间需要同步，误差在30秒内
2.停止顺序- 备库实例 -> 主库实例
3.启动顺序- 主库实例 -> 备库实例\`
  },
  "dsc": {
  	"dscStop": "1.切换dmdba用户：su - dmdba\n2.停止数据库：DmService${dsc_db_name} stop\n3.停止ASM实例：DmASMSvrServiceAsmsvr stop\n4.停止CSS服务：DmCSSServiceCss stop",
  	"dscStart": "1.切换dmdba用户：su - dmdba\n2.启动CSS服务DmCSSServiceCss start\n3.启动ASM实例：DmASMSvrServiceAsmsvr start\n4.启动数据库：DmService${dsc_db_name} start",
  	"dscManager": "连接到任一节点进行管理",
  	"dscDisql": "disql SYSDBA/'\"${sysinfo["SYSDBA"]}\"'@127.0.0.1:${port_num}",
  	"dscNote": \`1.集群启动前确保共享存储已挂载
2.停止顺序- 普通节点 -> 控制节点
3.启动顺序- 控制节点 -> 普通节点\`
  },
  "dscm": {
  	"dscStop": "1.切换dmdba用户：su - dmdba\n2.停止数据库：DmService${dsc_db_name} stop\n3.停止ASM实例：DmASMSvrmServiceAsmsvr stop\n4.停止CSS服务：DmCSSServiceCss stop",
  	"dscStart": "1.切换dmdba用户：su - dmdba\n2.启动CSS服务DmCSSServiceCss start\n3.启动ASM实例：DmASMSvrmServiceAsmsvr start\n4.启动数据库：DmService${dsc_db_name} start",
  	"dscManager": "连接到任一节点进行管理",
  	"dscDisql": "disql SYSDBA/'\"${sysinfo["SYSDBA"]}\"'@127.0.0.1:${port_num}",
  	"dscNote": \`1.集群启动前确保共享存储已挂载，
2.停止顺序- 普通节点 -> 控制节点
3.启动顺序- 控制节点 -> 普通节点\`
  }
},

"clusterConfigs": {
  "datawatch": {
  	"title": '数据守护集群',
  	"tableId": 'operations-datawatch',
  	"configParams": [
  		'TIME_ZONE=(480)',
  		'LANGUAGE=(en)',
  		'DM=($result)',
  		'[DM]',
  		'LOGIN_MODE=(1)',
  		'KEYWORDS=()'
  	],
  	"rwSeparateParams": [
  		'RW_SEPARATE=(1)',
  		'RW_PERCENT=(25)',
  		'SWITCH_TIMES=(3000)',
  		'SWITCH_INTERVAL=(200)'
  	],
  	"connectionUrl": 'jdbc:dm://DM',
  	"rwConnectionUrl": 'jdbc:dm://DM'
  },
  "raft": {
  	"title": 'RAFT集群',
  	"tableId": 'operations-raft',
  	"configParams": [
  		'TIME_ZONE=(480)',
  		'LANGUAGE=(en)',
  		'DM=($result)',
  		'[DM]',
  		'LOGIN_MODE=(1)',
  		'KEYWORDS=()'
  	],
  	"rwSeparateParams": [
  		'RW_SEPARATE=(1)',
  		'RW_PERCENT=(25)',
  		'SWITCH_TIMES=(3000)',
  		'SWITCH_INTERVAL=(200)'
  	],
  	"connectionUrl": 'jdbc:dm://DM',
  	"rwConnectionUrl": 'jdbc:dm://DM'
  },
  "dsc": {
  	"title": 'DSC集群',
  	"tableId": 'operations-dsc',
  	"configParams": [
  		'TIME_ZONE=(480)',
  		'LANGUAGE=(en)',
  		'DM=($result)',
  		'[DM]',
  		'SWITCH_TIMES=(60)',
  		'SWITCH_INTERVAL=(1000)',
  		'EP_SELECTOR=(0)',
  		'AUTO_RECONNECT=(1)'
  	],
  	"connectionUrl": 'jdbc:dm://DM',
  	"hasRwSeparate": false
  },
  "dscm": {
  	"title": 'DSC集群(DSCM)',
  	"tableId": 'operations-dscm',
  	"configParams": [
  		'TIME_ZONE=(480)',
  		'LANGUAGE=(en)',
  		'DM=($result)',
  		'[DM]',
  		'SWITCH_TIMES=(60)',
  		'SWITCH_INTERVAL=(1000)',
  		'EP_SELECTOR=(0)',
  		'AUTO_RECONNECT=(1)'
  	],
  	"connectionUrl": 'jdbc:dm://DM',
  	"hasRwSeparate": false
  }
},

"operationCommands": {
   "tablespaceUser": {
   	'createTablespace': 'CREATE TABLESPACE \"表空间名\" DATAFILE \\'表空间名.dbf\\' SIZE 5120 AUTOEXTEND ON NEXT 1024 CACHE=NORMAL;',
   	'createUser': 'CREATE USER \"用户名\" IDENTIFIED BY \"密码\"\n    DEFAULT TABLESPACE \"表空间名\"\n    DEFAULT INDEX TABLESPACE \"表空间名\";',
   	'grantPrivileges': 'GRANT PUBLIC,RESOURCE,VTI,SOI TO \"用户名\";\nALTER USER \"用户名\" LIMIT FAILED_LOGIN_ATTEMPS UNLIMITED, PASSWORD_LIFE_TIME UNLIMITED, PASSWORD_LOCK_TIME UNLIMITED;'
   },
   "logManagement": {
      'resizeLogfile': 'ALTER DATABASE RESIZE LOGFILE \\'${db_name}01.${rlog_postfix_name}\\' TO 2048;\nALTER DATABASE RESIZE LOGFILE \\'${db_name}02.${rlog_postfix_name}\\' TO 2048;'
   },
   "licenseManagement": {
   	'viewLicense': 'SELECT SERIES_NO,SERVER_SERIES,SERVER_TYPE,EXPIRED_DATE,AUTHORIZED_CUSTOMER FROM V\$LICENSE;',
   	'replaceLicense': '1. 将授权文件dm.key拷贝到安装目录的${env_dm_home}/bin目录下\n2. 修改文件权限- chown -R dmdba:dinstall dm.key\n3. 执行函数使授权生效： CALL SP_LOAD_LIC_INFO();'
   },
   "backupJob": {
   	'initJobEnv': 'SP_INIT_JOB_SYS(1);',
   	'fullBackupJob': \`${sysinfo[config_full]}\`,
   	'incrementalBackupJob': \`${sysinfo[config_inc]}\`
   },
   "statusMonitoring": {
   	'viewInstanceStatus': 'SELECT MODE\$,STATUS\$ FROM V\$INSTANCE;\n-- MODE\$- PRIMARY(主库), STANDBY(备库), NORMAL(普通)\n-- STATUS\\$- OPEN(正常), MOUNT(配置模式)',
   	'viewWatcherStatus': 'SELECT NAME, TYPE, STATUS FROM V\$DM_WATCHER;'
   	},
   "failoverCommands": {
      'normalSwitchover': \`-- 登录监视器\n${env_dm_home}/bin/dmmonitor $data_dir/$db_name/dmmonitor_${dw_group_name}.ini\nLOGIN\n-- 执行切换\nSWITCHOVER ${instance_name}\`,
      'primaryFailover': \`-- 登录监视器\n${env_dm_home}/bin/dmmonitor $data_dir/$db_name/dmmonitor_${dw_group_name}.ini\nLOGIN\n-- 执行接管\nTAKEOVER ${instance_name}\`
	}
},

"support": {
  "author": "袁xx",
  "mobile": "10000000000",
  "email": "yzj@dameng.com",
  "wechat": "10000000000",
  "manual": "${env_dm_home}/doc",
  "website": "https://www.dameng.com",
  "eco": "https://eco.dameng.com"
 }
};
EOF
	)
	printf "%s\n" "$json_data" >>"${software_dir}/${js_data}"
}
#========================================================
# 函数：pre_para_check
# 作用：安装前对脚本环境及参数进行前置校验
#========================================================
function pre_para_check() {
	local script_name="DMShellInstall"
	local dm_pkg="DMInstall.bin"
	# 1：推荐目录检查
	[[ $software_dir != "/soft" ]] && {
		printf "\n"
		color_printf yellow "建议将安装包和脚本放到 /soft 目录，并在该目录下执行，否则可能失败！"
	}
	# 2：禁止目录检查
	case $software_dir in
	/ | /root | "$env_dm_home")
		printf "\n"
		color_printf red "脚本和软件包不能放在 /、/root 或 $env_dm_home！"
		printf "\n"
		exit 1
		;;
	esac
	# 3：安装包存在性检查
	[[ ! -f $software_dir/$dm_iso_name && ! -f $software_dir/$dm_pkg ]] && {
		printf "\n"
		color_printf red "当前目录不存在 $dm_pkg，也未通过 -di 指定，无法继续安装！"
		printf "\n"
		exit 1
	}
	# 4：授权文件检查
	[[ -n "$key_path" && ! -f "$key_path" ]] && {
		printf "\n"
		color_printf red "指定 $key_path 授权文件不存在或者没有权限读取，请放置授权文件到家目录： $HOME "
		printf "\n"
		exit 1
	}
	# 5：脚本名检查
	[[ $(basename "$0") != "$script_name" ]] && {
		printf "\n"
		color_printf red "脚本名称必须为 $script_name！"
		printf "\n"
		exit 1
	}
	# 6：权限检查
	if [[ $EUID -ne 0 ]]; then
		sudo -l 2>/dev/null | grep -qE '\(ALL\).*NOPASSWD.*ALL' && {
			printf "\n"
			color_printf yellow "当前用户非 root，但拥有 (ALL) NOPASSWD: ALL 权限，继续执行。"
			return
		}
		printf "\n"
		color_printf red "当前用户非 root 且无 (ALL) NOPASSWD: ALL 权限，已退出！"
		printf "\n"
		exit 1
	fi
}
#========================================================
# 函数：para_clustr_check
# 作用：安装前对脚本环境及集群参数进行前置校验
#========================================================
function para_clustr_check() {
	local auth_required=true
	[[ -n $root_passwd || -n $ssh_key_path ]] && auth_required=false
	# 认证方式检查：必须提供 -rp 或 -skp
	if $auth_required; then
		die "错误：必须指定 -rp（root密码）或 -skp（SSH密钥路径）之一用于主机认证。"
	fi
	# 按模式检查必要参数
	case "$dm_install_mode" in
	datawatch)
		[[ -z $db_public_ip ]] && die "Datawatch 模式必须指定 -dpi（数据库公网IP列表）"
		[[ -z $db_mal_ip ]] && die "Datawatch 模式必须指定 -dmi（数据库内网IP列表）"
		;;
	raft)
		[[ -z $db_public_ip ]] && die "Raft 模式必须指定 -dpi（数据库公网IP列表）"
		[[ -z $xmal_ip ]] && die "Raft 模式必须指定 -xi（内网通信IP列表）"
		;;
	dsc)
		[[ -z $db_public_ip ]] && die "DSC 模式必须指定 -dpi（数据库公网IP列表）"
		[[ -z $db_mal_ip ]] && die "DSC 模式必须指定 -dmi（数据库内网IP列表）"
		[[ -z $dcr_disk ]] && die "DSC 模式必须指定 -dcd（DCR 共享磁盘）"
		[[ -z $vote_disk ]] && die "DSC 模式必须指定 -vod（投票磁盘）"
		[[ -z $data_disk ]] && die "DSC 模式必须指定 -dad（数据共享磁盘）"
		;;
	dscm)
		[[ -z $db_public_ip ]] && die "DSCM 模式必须指定 -dpi（数据库公网IP列表）"
		[[ -z $db_mal_ip ]] && die "DSCM 模式必须指定 -dmi（数据库内网IP列表）"
		[[ -z $dcr_disk ]] && die "DSCM 模式必须指定 -dcd（DCR 共享磁盘）"
		[[ -z $data_disk ]] && die "DSCM 模式必须指定 -dad（数据共享磁盘）"
		[[ -n $vote_disk ]] && die "DSCM 模式不支持指定 -vod（投票磁盘），请移除该参数"
		;;
	esac
	# 统一检查公网/内网 IP 数量一致性（适用于所有集群模式）
	local pubip_count=0
	local malip_count=0
	# 累加各类节点的公网/内网 IP 数量
	((pubip_count += ${#db_pubip_arr[@]}))
	((pubip_count += ${#as_pubip_arr[@]}))
	((pubip_count += ${#sy_pubip_arr[@]}))
	((malip_count += ${#db_malip_arr[@]}))
	((malip_count += ${#as_malip_arr[@]}))
	((malip_count += ${#sy_malip_arr[@]}))
	if [ $pubip_count -ne $malip_count ]; then
		die "IP 地址数量不匹配：公网 IP 数量=$pubip_count，内网 IP 数量=$malip_count。请检查 -dpi, -dmi, -api, -ami, -spi, -smi 参数。"
	fi
	# 检查节点数量合法性
	case "$dm_install_mode" in
	datawatch)
		if ((pubip_count < 2 || pubip_count > 24)); then
			die "Datawatch 模式节点数必须为 2~24，当前: $pubip_count"
		fi
		;;
	raft)
		if ! [[ $pubip_count =~ ^(3|5|7|9)$ ]]; then
			die "Raft 模式节点数必须为 3/5/7/9，当前: $pubip_count"
		fi
		;;
	dsc | dscm)
		if ((pubip_count < 2 || pubip_count > 16)); then
			die "${dm_install_mode} 模式节点数必须为 2~16，当前: $pubip_count"
		fi
		;;
	esac
	[[ $ARG_CHECK_FLAG -eq 0 ]] && printf "\n"
}
#========================================================
# 函数：main
# 作用：程序统一入口
#========================================================
function main() {
	# 记录启动命令到日志
	node_cmd=$(printf "\n%s %s\n" "$0" "$(printf "%q " "$@")" | tee -a "$dminstalllog")
	# 初始化：打印标题、解析参数、检查系统、安装sshpass（如指定）
	script_print
	parse_args "$@"
	define_os_flag
	command_check
	[[ $only_install_sshpass =~ [Yy] ]] && install_sshpass
	# 首节点前置操作：创建用户（如指定）、参数检查
	[[ $node_num -eq 1 ]] && {
		[[ $only_create_user =~ [Yy] ]] && create_os_user
		pre_para_check
	}
	# 如果基础参数校验通过，则确定安装模式（单机/集群）
	[[ $ARG_CHECK_FLAG -eq 1 ]] && install_mode
	# 集群模式下：检查集群参数、IP和磁盘
	[[ $dm_install_mode =~ ^(datawatch|raft|dsc|dscm)$ ]] && {
		para_clustr_check
		validate_ip
		validate_disk
	}
	# 参数校验失败则退出
	[[ $ARG_CHECK_FLAG -eq 0 ]] && {
		printf "\n"
		color_printf red "参数校验不通过！请检查屏幕上输出的参数异常信息或者查看 ${dminstalllog} 文件获取异常参数信息，处理后再次校验！"
		printf "\n"
		exit 1
	}
	# 临时修改密码复杂度策略
	conf_pwq_temp
	# 根据安装模式，重定义参数
	redefine_para
	# 安装工具
	pkg_install
	# 集群首节点输出配置提示
	[[ $dm_install_mode =~ ^(datawatch|raft|dsc|dscm)$ && $node_num -eq 1 ]] && {
		# 配置互信
		[[ -z $ssh_key_path && $is_sshpass =~ [Nn] ]] && user_ssh_trust
		# 发送数据库安装包和脚本到其他节点
		send_node_scripts
		# 开始配置
		log_print "开始配置节点: ${all_pubip_arr[0]}"
		color_printf yellow "节点 ${all_pubip_arr[0]} 开始配置"
	}
	# 生成部署数据、输出系统信息、配置系统环境
	generate_json_data1
	print_sysinfo
	conf_os_env
	# 安装流程（非仅配置OS时）
	if [[ $only_conf_os == "N" ]]; then
		install_soft
		# 非仅安装前置时，根据模式初始化数据库
		if [[ $install_until_db == "N" ]]; then
			case $dm_install_mode in
			single) init_si ;;
			datawatch | raft)
				[[ $node_num -ne -1 ]] && init_si
				init_dw_rf
				;;
			dsc | dscm) init_dsc ;;
			esac
			# 首节点额外配置：大页、修改DB参数、生成第二阶段数据
			[[ $node_num -eq 1 ]] && {
				[[ $huge_flag == "Y" ]] && {
					log_print "配置大页内存"
					conf_hugepage
				}
				mod_db_pwd
				generate_json_data2
			}
		fi
	fi
	# 首节点统一重启所有主机
	[[ $node_num -eq 1 ]] && restart_hosts
}
# 启动主函数并记录日志
main "$@" | tee -a "$dminstalllog"
